Replace native confirms with Ladill custom dialogs.
Deploy Ladill Care / deploy (push) Successful in 1m24s

This commit is contained in:
isaacclad
2026-07-24 20:56:01 +00:00
parent 8cd0312045
commit f39468b4a3
9 changed files with 223 additions and 10 deletions
@@ -114,7 +114,7 @@
<a href="{{ route('care.visits.mar', $visit) }}" class="mr-2 text-indigo-600 hover:text-indigo-800">MAR</a>
@endif
@if ($canPlace && $visit->care_unit_id)
<form method="POST" action="{{ route('care.visits.placement.destroy', $visit) }}" class="inline" onsubmit="return confirm('Discharge placement and free the bed?')">
<form method="POST" action="{{ route('care.visits.placement.destroy', $visit) }}" class="inline" data-confirm="Discharge placement and free the bed?" data-confirm-title="Discharge placement and free the bed?" data-confirm-label="Confirm">
@csrf @method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-800">Discharge bed</button>
</form>
@@ -221,7 +221,7 @@
<td class="py-3 pr-4">{{ $occupant?->patient?->fullName() ?? '—' }}</td>
<td class="py-3 text-right">
@if ($canManageUnit ?? false)
<form method="POST" action="{{ route('care.care-units.beds.destroy', [$unit, $bed]) }}" class="inline" onsubmit="return confirm('Remove this bed?')">
<form method="POST" action="{{ route('care.care-units.beds.destroy', [$unit, $bed]) }}" class="inline" data-confirm="Remove this bed?" data-confirm-title="Remove this bed?" data-confirm-label="Remove">
@csrf @method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-800">Remove</button>
</form>
@@ -67,8 +67,7 @@
<button type="submit" class="btn-primary w-full">Save changes</button>
</form>
<form method="POST" action="{{ route('care.practitioners.destroy', $practitioner) }}" class="mt-4"
onsubmit="return confirm('Remove this practitioner from Care? Existing appointments keep their history.')">
<form method="POST" action="{{ route('care.practitioners.destroy', $practitioner) }}" class="mt-4" data-confirm="Remove this practitioner from Care? Existing appointments keep their history." data-confirm-title="Are you sure?" data-confirm-label="Remove">
@csrf
@method('DELETE')
<button type="submit" class="btn-secondary w-full text-rose-700">Remove practitioner</button>
@@ -102,7 +102,7 @@
</form>
@if ($assignment)
<form method="POST" action="{{ route('care.staff-assignments.destroy', $assignment) }}" class="mt-4" onsubmit="return confirm('Remove this unit assignment?')">
<form method="POST" action="{{ route('care.staff-assignments.destroy', $assignment) }}" class="mt-4" data-confirm="Remove this unit assignment?" data-confirm-title="Remove this unit assignment?" data-confirm-label="Remove">
@csrf @method('DELETE')
<button type="submit" class="text-sm font-medium text-red-600 hover:text-red-800">Remove unit assignment</button>
</form>
+1 -1
View File
@@ -158,7 +158,7 @@
<button type="submit" class="rounded-lg border border-rose-200 bg-white px-4 py-2 text-sm text-rose-700 hover:bg-rose-50">Revoke token</button>
</form>
@endif
<form method="POST" action="{{ route('care.devices.destroy', $device) }}" onsubmit="return confirm('Remove this device?')">
<form method="POST" action="{{ route('care.devices.destroy', $device) }}" data-confirm="Remove this device?" data-confirm-title="Remove this device?" data-confirm-label="Remove">
@csrf @method('DELETE')
<button type="submit" class="rounded-lg border border-rose-200 bg-white px-4 py-2 text-sm text-rose-700 hover:bg-rose-50">Delete device</button>
</form>
@@ -59,7 +59,7 @@
{{ $memberLabels[$entry->member_id] ?? ($entry->member?->user_ref ?? 'Staff') }}
</div>
@if ($canManage)
<form method="POST" action="{{ route('care.care-units.roster.destroy', [$unit, $entry]) }}" class="mt-1" onsubmit="return confirm('Remove from roster?')">
<form method="POST" action="{{ route('care.care-units.roster.destroy', [$unit, $entry]) }}" class="mt-1" data-confirm="Remove from roster?" data-confirm-title="Remove from roster?" data-confirm-label="Remove">
@csrf
@method('DELETE')
<button type="submit" class="text-[11px] text-red-600 hover:text-red-800">Remove</button>
@@ -76,7 +76,7 @@
@endif
</div>
@if ($canManage)
<form method="POST" action="{{ route('care.specialty.dentistry.images.void', [$workspaceVisit, $image]) }}" onsubmit="return confirm('Void this image?')">
<form method="POST" action="{{ route('care.specialty.dentistry.images.void', [$workspaceVisit, $image]) }}" data-confirm="Void this image?" data-confirm-title="Void this image?" data-confirm-label="Confirm">
@csrf
<button type="submit" class="text-rose-600 hover:underline">Void</button>
</form>
@@ -33,7 +33,7 @@
</form>
@endif
@if ($plan->status !== 'completed')
<form method="POST" action="{{ route('care.specialty.dentistry.plan.cancel', $workspaceVisit) }}" onsubmit="return confirm('Cancel this plan?')">
<form method="POST" action="{{ route('care.specialty.dentistry.plan.cancel', $workspaceVisit) }}" data-confirm="Cancel this plan?" data-confirm-title="Cancel this plan?" data-confirm-label="Confirm">
@csrf
<button type="submit" class="rounded-lg border border-slate-200 px-3 py-1.5 text-sm font-medium text-slate-700">Cancel plan</button>
</form>
@@ -55,7 +55,7 @@
<p class="text-xs text-slate-500">Priority {{ $item->priority }} · {{ ucfirst($item->status) }} · {{ number_format($item->estimate_minor / 100, 2) }}</p>
</div>
@if ($canManage && ! in_array($item->status, ['done', 'cancelled'], true))
<form method="POST" action="{{ route('care.specialty.dentistry.plan-items.cancel', [$workspaceVisit, $item]) }}" onsubmit="return confirm('Cancel this item?')">
<form method="POST" action="{{ route('care.specialty.dentistry.plan-items.cancel', [$workspaceVisit, $item]) }}" data-confirm="Cancel this item?" data-confirm-title="Cancel this item?" data-confirm-label="Confirm">
@csrf
<button type="submit" class="text-xs font-medium text-rose-600">Cancel item</button>
</form>
@@ -58,5 +58,6 @@
@include('partials.afia')
@include('partials.wallet-topup-modal', ['openOnLoad' => (bool) session('topup_required')])
@include('partials.sso-keepalive')
@include('partials.confirm-prompt')
</body>
</html>
@@ -0,0 +1,213 @@
{{--
Ladill custom confirm dialog (replaces native window.confirm for app UX).
Usage:
1) Forms: <form data-confirm="Delete this item?" data-confirm-title="Delete?" ...>
2) JS: const ok = await window.ladillConfirm({ title, message, confirmLabel, variant })
Bottom sheet on mobile, centered card on desktop same pattern as x-modal.
--}}
<div id="ladill-confirm-root" class="fixed inset-0 z-[60] hidden items-end sm:items-center sm:justify-center"
style="background: rgba(0,0,0,0.45); backdrop-filter: blur(3px);"
role="dialog" aria-modal="true" aria-labelledby="ladill-confirm-title">
<div id="ladill-confirm-backdrop" class="absolute inset-0"></div>
<div id="ladill-confirm-panel"
class="relative z-10 w-full max-h-[90vh] translate-y-full overflow-y-auto rounded-t-3xl bg-white shadow-2xl opacity-0 transition duration-200 sm:mb-6 sm:max-h-[85vh] sm:max-w-md sm:translate-y-2 sm:scale-95 sm:rounded-2xl sm:border sm:border-gray-200">
<div class="flex justify-center pb-1 pt-3 sm:hidden">
<div class="h-1 w-10 rounded-full bg-slate-200"></div>
</div>
<div class="px-5 pb-6 pt-2 sm:px-6 sm:pb-6 sm:pt-4">
<div class="flex flex-col items-center text-center sm:items-start sm:text-left">
<div id="ladill-confirm-icon" class="flex h-12 w-12 items-center justify-center rounded-full bg-red-100 text-red-600">
<svg class="h-6 w-6" fill="none" stroke="currentColor" stroke-width="1.75" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/>
</svg>
</div>
<h2 id="ladill-confirm-title" class="mt-4 text-lg font-semibold text-slate-900">Are you sure?</h2>
<p id="ladill-confirm-message" class="mt-2 whitespace-pre-line text-sm leading-relaxed text-slate-500"></p>
</div>
<div class="mt-6 flex flex-col-reverse gap-2.5 sm:flex-row sm:justify-end">
<button type="button" id="ladill-confirm-cancel"
class="rounded-xl border border-slate-200 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-50">
Cancel
</button>
<button type="button" id="ladill-confirm-ok"
class="rounded-xl bg-red-600 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-red-700">
Confirm
</button>
</div>
</div>
</div>
</div>
<script>
(function () {
if (window.__ladillConfirmInstalled) {
return;
}
window.__ladillConfirmInstalled = true;
let resolveAsk = null;
const root = () => document.getElementById('ladill-confirm-root');
const panel = () => document.getElementById('ladill-confirm-panel');
const titleEl = () => document.getElementById('ladill-confirm-title');
const messageEl = () => document.getElementById('ladill-confirm-message');
const okBtn = () => document.getElementById('ladill-confirm-ok');
const cancelBtn = () => document.getElementById('ladill-confirm-cancel');
const iconEl = () => document.getElementById('ladill-confirm-icon');
function close(result) {
const r = root();
const p = panel();
if (!r || !p) {
if (resolveAsk) {
resolveAsk(result);
resolveAsk = null;
}
return;
}
p.classList.add('translate-y-full', 'opacity-0', 'sm:translate-y-2', 'sm:scale-95');
p.classList.remove('translate-y-0', 'opacity-100', 'sm:scale-100');
setTimeout(() => {
r.classList.add('hidden');
r.classList.remove('flex');
document.body.classList.remove('overflow-y-hidden');
if (resolveAsk) {
resolveAsk(result);
resolveAsk = null;
}
}, 180);
}
function openDialog(options) {
const opts = options || {};
return new Promise((resolve) => {
if (resolveAsk) {
resolveAsk(false);
}
resolveAsk = resolve;
const r = root();
const p = panel();
if (!r || !p) {
resolve(window.__ladillNativeConfirm
? window.__ladillNativeConfirm(opts.message || opts.title || 'Are you sure?')
: false);
return;
}
titleEl().textContent = opts.title || 'Are you sure?';
const msg = opts.message || '';
messageEl().textContent = msg;
messageEl().classList.toggle('hidden', !msg);
okBtn().textContent = opts.confirmLabel || 'Confirm';
cancelBtn().textContent = opts.cancelLabel || 'Cancel';
const primary = (opts.variant || 'danger') === 'primary';
iconEl().className = 'flex h-12 w-12 items-center justify-center rounded-full ' +
(primary ? 'bg-violet-100 text-violet-600' : 'bg-red-100 text-red-600');
okBtn().className = 'rounded-xl px-4 py-2.5 text-sm font-semibold text-white transition ' +
(primary ? 'bg-violet-600 hover:bg-violet-700' : 'bg-red-600 hover:bg-red-700');
r.classList.remove('hidden');
r.classList.add('flex');
document.body.classList.add('overflow-y-hidden');
requestAnimationFrame(() => {
p.classList.remove('translate-y-full', 'opacity-0', 'sm:translate-y-2', 'sm:scale-95');
p.classList.add('translate-y-0', 'opacity-100', 'sm:scale-100');
});
okBtn().focus();
});
}
window.ladillConfirm = openDialog;
// Prefer custom dialog; keep native as emergency fallback only.
if (!window.__ladillNativeConfirm) {
window.__ladillNativeConfirm = window.confirm.bind(window);
}
document.addEventListener('click', function (e) {
if (e.target.closest('#ladill-confirm-cancel') || e.target.closest('#ladill-confirm-backdrop')) {
close(false);
}
if (e.target.closest('#ladill-confirm-ok')) {
close(true);
}
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && root() && !root().classList.contains('hidden')) {
close(false);
}
});
// Forms: data-confirm="…" replaces onsubmit="return confirm(…)"
document.addEventListener('submit', function (e) {
const form = e.target;
if (!(form instanceof HTMLFormElement)) {
return;
}
if (form.dataset.ladillConfirming === '1') {
return;
}
const message = form.getAttribute('data-confirm');
if (message === null || message === '') {
return;
}
e.preventDefault();
e.stopImmediatePropagation();
openDialog({
title: form.getAttribute('data-confirm-title') || 'Are you sure?',
message: message,
confirmLabel: form.getAttribute('data-confirm-label') || 'Confirm',
cancelLabel: form.getAttribute('data-confirm-cancel') || 'Cancel',
variant: form.getAttribute('data-confirm-variant') || 'danger',
}).then(function (ok) {
if (!ok) {
return;
}
form.dataset.ladillConfirming = '1';
// Native submit skips the submit event → no re-entry.
HTMLFormElement.prototype.submit.call(form);
queueMicrotask(function () {
delete form.dataset.ladillConfirming;
});
});
}, true);
// Buttons/links: data-confirm on non-submit controls that should gate a click.
document.addEventListener('click', function (e) {
const el = e.target.closest('[data-confirm-click]');
if (!el || el.closest('form[data-confirm]')) {
return;
}
const message = el.getAttribute('data-confirm-click');
if (message === null || message === '') {
return;
}
e.preventDefault();
e.stopPropagation();
openDialog({
title: el.getAttribute('data-confirm-title') || 'Are you sure?',
message: message,
confirmLabel: el.getAttribute('data-confirm-label') || 'Confirm',
cancelLabel: el.getAttribute('data-confirm-cancel') || 'Cancel',
variant: el.getAttribute('data-confirm-variant') || 'danger',
}).then(function (ok) {
if (!ok) {
return;
}
if (el.tagName === 'A' && el.href) {
window.location.href = el.href;
return;
}
if (el.form) {
el.form.dataset.ladillConfirming = '1';
HTMLFormElement.prototype.submit.call(el.form);
}
});
}, true);
})();
</script>