fix(ui): replace native alert/confirm with showConfirm/showToast helpers

Daniel flagged the native browser confirm() dialogs in Extensions as ugly
and incompatible with the app's design. There was also a stray alert()
in calculators.js resus-meds weight validation.

Replaced:
- extensions.js: confirmDelete → showConfirm(..., {confirmText: 'Move to trash'})
- extensions.js: confirmPurge  → showConfirm(..., {danger: true, confirmText: 'Delete permanently'})
- calculators.js:2060 alert() → showToast(..., 'error')

All three helpers (showConfirm, showToast) are already defined as globals
in public/js/app.js. The design already had a modal — I should have used
it from the start.

Audit confirmation: `grep -rnE '\b(alert|confirm|prompt)\s*\(' public/`
now returns only comment references and the showConfirm definition
itself. No native dialogs remain anywhere in the frontend.

Playwright test updated to click the in-app modal's #confirm-modal-ok
and #confirm-modal-cancel buttons instead of intercepting page.on('dialog').
This commit is contained in:
Daniel 2026-04-22 19:29:53 +02:00
parent 8ea79c7f30
commit b37c565cf0
3 changed files with 36 additions and 26 deletions

View file

@ -149,9 +149,9 @@ test.describe('Extensions — CRUD', () => {
await openTab(page);
await fillForm(page, { location: 'Loc', name: 'To delete', number: '9999', type: 'extension' });
// Auto-accept the confirm() dialog
page.once('dialog', d => d.accept());
await page.locator('.ext-card button[data-ext-action="delete"]').first().click();
// In-app modal — click confirm
await page.click('#confirm-modal-ok');
// Gone from active list
await expect(page.locator('#ext-list')).toContainText(/No entries yet/i, { timeout: 5000 });
@ -188,8 +188,8 @@ test.describe('Extensions — CRUD', () => {
await page.click('#ext-trash-btn');
await expect(page.locator('.ext-card')).toContainText('Gone forever');
page.once('dialog', d => d.accept());
await page.locator('.ext-card button[data-ext-action="purge"]').first().click();
await page.click('#confirm-modal-ok');
// Trash is empty
await expect(page.locator('#ext-list')).toContainText(/Trash is empty/i, { timeout: 5000 });
@ -203,9 +203,9 @@ test.describe('Extensions — CRUD', () => {
await openTab(page);
await fillForm(page, { location: 'Loc', name: 'Stays', number: '8888', type: 'extension' });
// User cancels the confirm dialog
page.once('dialog', d => d.dismiss());
// User cancels the modal
await page.locator('.ext-card button[data-ext-action="delete"]').first().click();
await page.click('#confirm-modal-cancel');
// Still there
await expect(page.locator('.ext-card')).toContainText('Stays');

View file

@ -2057,7 +2057,7 @@
var weightEl = document.getElementById('resus-weight');
var resultDiv = document.getElementById('resus-result');
var w = parseFloat(weightEl.value);
if (!w || w <= 0) { alert('Please enter a valid weight.'); return; }
if (!w || w <= 0) { showToast('Please enter a valid weight.', 'error'); return; }
var html = '<div style="margin-bottom:12px;font-size:14px;font-weight:700;color:var(--g800);">Doses for ' + w + ' kg patient</div>';
html += '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px;font-size:11px;">';

View file

@ -228,16 +228,21 @@
function confirmDelete(id) {
var item = _items.filter(function (x) { return x.id === id; })[0];
if (!item) return;
if (!confirm('Move "' + item.name + ' (' + item.number + ')" to trash?\n\nYou can restore it later from the trash view.')) return;
fetch('/api/extensions/' + id, { method: 'DELETE', headers: getAuthHeaders() })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d.success) { showToast(d.error || 'Delete failed', 'error'); return; }
showToast('Moved to trash', 'success');
load();
loadTrashCount();
})
.catch(function () { showToast('Delete failed', 'error'); });
showConfirm(
'Move "' + item.name + ' (' + item.number + ')" to trash? You can restore it later from the trash view.',
function () {
fetch('/api/extensions/' + id, { method: 'DELETE', headers: getAuthHeaders() })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d.success) { showToast(d.error || 'Delete failed', 'error'); return; }
showToast('Moved to trash', 'success');
load();
loadTrashCount();
})
.catch(function () { showToast('Delete failed', 'error'); });
},
{ confirmText: 'Move to trash' }
);
}
function restoreItem(id) {
@ -255,16 +260,21 @@
function confirmPurge(id) {
var item = _items.filter(function (x) { return x.id === id; })[0];
if (!item) return;
if (!confirm('Permanently delete "' + item.name + ' (' + item.number + ')"?\n\nThis cannot be undone.')) return;
fetch('/api/extensions/' + id + '/purge', { method: 'DELETE', headers: getAuthHeaders() })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d.success) { showToast(d.error || 'Delete failed', 'error'); return; }
showToast('Permanently deleted', 'success');
load();
loadTrashCount();
})
.catch(function () { showToast('Delete failed', 'error'); });
showConfirm(
'Permanently delete "' + item.name + ' (' + item.number + ')"? This cannot be undone.',
function () {
fetch('/api/extensions/' + id + '/purge', { method: 'DELETE', headers: getAuthHeaders() })
.then(function (r) { return r.json(); })
.then(function (d) {
if (!d.success) { showToast(d.error || 'Delete failed', 'error'); return; }
showToast('Permanently deleted', 'success');
load();
loadTrashCount();
})
.catch(function () { showToast('Delete failed', 'error'); });
},
{ danger: true, confirmText: 'Delete permanently' }
);
}
function toggleTrashMode() {