diff --git a/src/server/templates/admin.js b/src/server/templates/admin.js index cf89ae6..c7470c8 100644 --- a/src/server/templates/admin.js +++ b/src/server/templates/admin.js @@ -71,16 +71,19 @@ function buildEditQtyRow(name, qty) { minusBtn.dataset.adminAction = 'edit-qty'; minusBtn.dataset.mealName = name; minusBtn.dataset.delta = '-1'; + minusBtn.setAttribute('aria-label', 'Remove one ' + name); minusBtn.innerHTML = '−'; const span = document.createElement('span'); span.textContent = qty; + span.setAttribute('aria-label', 'Quantity of ' + name); const plusBtn = document.createElement('button'); plusBtn.type = 'button'; plusBtn.dataset.adminAction = 'edit-qty'; plusBtn.dataset.mealName = name; plusBtn.dataset.delta = '1'; + plusBtn.setAttribute('aria-label', 'Add one ' + name); plusBtn.textContent = '+'; wrap.appendChild(minusBtn); @@ -311,6 +314,7 @@ function downloadSummaryPdf() { const week = currentAdminWeek || WEEK; const btn = document.getElementById('admin-pdf-btn'); if (btn) btn.disabled = true; + window.FormUtils.clearStatus('admin'); adminFetch(ADMIN_PDF_URL + '?week=' + encodeURIComponent(week), { headers: adminHeaders() }) .then(r => r.json().then(data => ({ ok: r.ok, status: r.status, data }))) .then(({ ok, status, data }) => { @@ -319,19 +323,34 @@ function downloadSummaryPdf() { if (typeof data.url === 'string' && data.url.startsWith('https://')) { window.location.href = data.url; } else { - alert('Could not fetch the summary PDF: invalid URL'); + window.FormUtils.showStatus('Could not fetch the summary PDF: invalid URL', { + tone: 'error', + scope: 'admin', + }); } } else if (status === 404) { - alert('No summary PDF for ' + week + ' yet — it is generated when the week closes on Thursday.'); + window.FormUtils.showStatus( + 'No summary PDF for ' + week + ' yet — it is generated when the week closes on Thursday.', + { tone: 'info', scope: 'admin' } + ); } else { - alert('Could not fetch the summary PDF: ' + (data.error || 'unknown error')); + window.FormUtils.showStatus( + 'Could not fetch the summary PDF: ' + (data.error || 'unknown error'), + { tone: 'error', scope: 'admin' } + ); } }) - .catch(() => alert('Could not fetch the summary PDF.')) + .catch(() => { + window.FormUtils.showStatus('Could not fetch the summary PDF.', { + tone: 'error', + scope: 'admin', + }); + }) .finally(() => { if (btn) btn.disabled = false; }); } function adminStartEdit(email) { + window.FormUtils.clearStatus('admin'); const requestedWeek = currentAdminWeek; setAdminActionDisabled('start-edit', true); adminFetch(ADMIN_URL + '?week=' + encodeURIComponent(requestedWeek), { headers: adminHeaders() }) @@ -349,7 +368,12 @@ function adminStartEdit(email) { order.items.forEach(i => { editQuantities[i.name] = i.quantity; }); renderAdminTable(data); }) - .catch(() => alert('Failed to load this order for editing.')) + .catch(() => { + window.FormUtils.showStatus('Failed to load this order for editing.', { + tone: 'error', + scope: 'admin', + }); + }) .finally(() => setAdminActionDisabled('start-edit', false)); } @@ -376,11 +400,13 @@ function adminSaveEdit(email) { .filter(([, q]) => q > 0) .map(([name, quantity]) => ({ name, quantity })); if (items.length === 0) { + // confirm() retained deliberately for destructive zero-qty → delete guard. if (confirm('All quantities are zero. Delete this order instead?')) { adminDeleteOrder(email, ''); } return; } + window.FormUtils.clearStatus('admin'); setAdminActionDisabled('save-edit', true); adminFetch(ADMIN_URL, { method: 'PUT', @@ -389,13 +415,20 @@ function adminSaveEdit(email) { }) .then(r => { if (!r.ok) return r.json().then(d => { throw new Error(d.error); }); return r.json(); }) .then(() => { editingOrder = null; editQuantities = {}; loadWeekOrders(currentAdminWeek); }) - .catch(e => alert('Failed to update: ' + e.message)) + .catch(e => { + window.FormUtils.showStatus('Failed to update: ' + e.message, { + tone: 'error', + scope: 'admin', + }); + }) .finally(() => setAdminActionDisabled('save-edit', false)); } function adminDeleteOrder(email, name) { const label = name ? name + ' (' + email + ')' : email; + // confirm() retained deliberately for destructive delete guard. if (!confirm('Delete order for ' + label + '?')) return; + window.FormUtils.clearStatus('admin'); setAdminActionDisabled('delete', true); adminFetch(ADMIN_URL + '?week=' + encodeURIComponent(currentAdminWeek) + '&email=' + encodeURIComponent(email), { method: 'DELETE', @@ -403,7 +436,12 @@ function adminDeleteOrder(email, name) { }) .then(r => { if (!r.ok) return r.json().then(d => { throw new Error(d.error); }); return r.json(); }) .then(() => loadWeekOrders(currentAdminWeek)) - .catch(e => alert('Failed to delete: ' + e.message)) + .catch(e => { + window.FormUtils.showStatus('Failed to delete: ' + e.message, { + tone: 'error', + scope: 'admin', + }); + }) .finally(() => setAdminActionDisabled('delete', false)); } diff --git a/src/server/templates/form.css b/src/server/templates/form.css index e4dc97f..0e35cd9 100644 --- a/src/server/templates/form.css +++ b/src/server/templates/form.css @@ -152,6 +152,28 @@ header p { font-size: 0.85rem; opacity: 0.8; } overflow: hidden; } +.meal-desc.is-expanded { + display: block; + -webkit-line-clamp: unset; + overflow: visible; +} + +.meal-desc-toggle { + display: inline-block; + margin: 0 0 4px; + padding: 0; + border: none; + background: none; + color: var(--color-brand); + font-size: 0.8rem; + font-weight: 600; + font-family: inherit; + cursor: pointer; + text-decoration: underline; +} + +.meal-desc-toggle:hover { opacity: 0.85; } + .qty-control { display: flex; align-items: center; @@ -367,6 +389,42 @@ header p { font-size: 0.85rem; opacity: 0.8; } .qty-control .qty { width: 44px; height: 44px; } .edit-qty button { width: 44px; height: 44px; } .edit-qty span { width: 44px; height: 44px; line-height: 44px; } + .btn-chip { min-height: 44px; padding: 10px 16px; } +} + +.form-status { + max-width: 1400px; + margin: 0 auto 8px; +} + +.sticky-footer .form-status { + margin-bottom: 8px; +} + +.admin-panel > .form-status { + margin: 0 0 12px; +} + +.form-status-msg:empty { display: none; } + +.form-status-msg { + padding: 8px 12px; + border-radius: var(--radius-control); + font-size: 0.85rem; + font-weight: 500; + text-align: center; +} + +.form-status-error { + background: #fef2f2; + color: #991b1b; + border: 1px solid #fecaca; +} + +.form-status-info { + background: #eff6ff; + color: #1d4ed8; + border: 1px solid #bfdbfe; } .discount-banner { diff --git a/src/server/templates/form.html.j2 b/src/server/templates/form.html.j2 index 84f3ee1..0f1a324 100644 --- a/src/server/templates/form.html.j2 +++ b/src/server/templates/form.html.j2 @@ -85,7 +85,7 @@