fix(form): address review findings

This commit is contained in:
Adam Moussa 2026-07-30 20:01:16 -04:00 • committed by Adam Moussa
parent dea398626f
commit e60b8e26dd
6 changed files with 72 additions and 21 deletions

View file

@ -97,9 +97,9 @@ def generate_form(
return template.render( return template.render(
config=form_config, config=form_config,
config_json=_json_for_script(form_config), config_json=_json_for_script(form_config),
css=(TEMPLATES_DIR / "form.css").read_text(), css=(TEMPLATES_DIR / "form.css").read_text(encoding="utf-8"),
form_js=(TEMPLATES_DIR / "form.js").read_text(), form_js=(TEMPLATES_DIR / "form.js").read_text(encoding="utf-8"),
admin_js=(TEMPLATES_DIR / "admin.js").read_text(), admin_js=(TEMPLATES_DIR / "admin.js").read_text(encoding="utf-8"),
week=week, week=week,
scraped_at=scraped_at, scraped_at=scraped_at,
deadline=deadline, deadline=deadline,

View file

@ -4,11 +4,13 @@ const ADMIN_URL = window.ADMIN_URL;
const ADMIN_PDF_URL = window.ADMIN_PDF_URL; const ADMIN_PDF_URL = window.ADMIN_PDF_URL;
const MEALS = window.MEALS; const MEALS = window.MEALS;
const WEEK = window.WEEK; const WEEK = window.WEEK;
const ADMIN_REQUEST_TIMEOUT_MS = 10000;
let currentAdminWeek = ''; let currentAdminWeek = '';
let editingOrder = null; let editingOrder = null;
let editQuantities = {}; let editQuantities = {};
let lastAdminData = null; let lastAdminData = null;
let latestAdminRequest = 0;
function adminHeaders() { function adminHeaders() {
const h = {}; const h = {};
@ -17,8 +19,22 @@ function adminHeaders() {
return h; return h;
} }
function adminFetch(url, options = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), ADMIN_REQUEST_TIMEOUT_MS);
return fetch(url, { ...options, signal: controller.signal })
.finally(() => clearTimeout(timeout));
}
function setAdminActionDisabled(action, disabled) {
document.querySelectorAll('[data-admin-action="' + action + '"]').forEach(btn => {
btn.disabled = disabled;
});
}
function loadWeekOrders(week) { function loadWeekOrders(week) {
if (!week) return; if (!week) return;
const requestId = ++latestAdminRequest;
currentAdminWeek = week; currentAdminWeek = week;
editingOrder = null; editingOrder = null;
editQuantities = {}; editQuantities = {};
@ -27,14 +43,20 @@ function loadWeekOrders(week) {
document.getElementById('admin-empty').classList.add('is-hidden'); document.getElementById('admin-empty').classList.add('is-hidden');
document.getElementById('admin-stats').textContent = ''; document.getElementById('admin-stats').textContent = '';
fetch(ADMIN_URL + '?week=' + encodeURIComponent(week), { headers: adminHeaders() }) adminFetch(ADMIN_URL + '?week=' + encodeURIComponent(week), { headers: adminHeaders() })
.then(r => { .then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status); if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json(); return r.json();
}) })
.then(data => renderAdminTable(data)) .then(data => {
if (requestId === latestAdminRequest && week === currentAdminWeek) {
renderAdminTable(data);
}
})
.catch(() => { .catch(() => {
if (requestId === latestAdminRequest && week === currentAdminWeek) {
tbody.innerHTML = '<tr><td colspan="5" class="admin-error">Failed to load orders.</td></tr>'; tbody.innerHTML = '<tr><td colspan="5" class="admin-error">Failed to load orders.</td></tr>';
}
}); });
} }
@ -245,7 +267,7 @@ function renderAdminTable(data) {
const t1 = document.createElement('td'); const t1 = document.createElement('td');
t1.textContent = 'Total'; t1.textContent = 'Total';
const t2 = document.createElement('td'); const t2 = document.createElement('td');
t2.textContent = mealCount + ' meals'; t2.textContent = mealCount + (mealCount === 1 ? ' meal' : ' meals');
const t3 = document.createElement('td'); const t3 = document.createElement('td');
t3.className = 'total-cell'; t3.className = 'total-cell';
t3.textContent = '$' + data.grand_total.toFixed(2); t3.textContent = '$' + data.grand_total.toFixed(2);
@ -289,7 +311,7 @@ function downloadSummaryPdf() {
const week = currentAdminWeek || WEEK; const week = currentAdminWeek || WEEK;
const btn = document.getElementById('admin-pdf-btn'); const btn = document.getElementById('admin-pdf-btn');
if (btn) btn.disabled = true; if (btn) btn.disabled = true;
fetch(ADMIN_PDF_URL + '?week=' + encodeURIComponent(week), { headers: adminHeaders() }) adminFetch(ADMIN_PDF_URL + '?week=' + encodeURIComponent(week), { headers: adminHeaders() })
.then(r => r.json().then(data => ({ ok: r.ok, status: r.status, data }))) .then(r => r.json().then(data => ({ ok: r.ok, status: r.status, data })))
.then(({ ok, status, data }) => { .then(({ ok, status, data }) => {
if (ok && data.url) { if (ok && data.url) {
@ -310,7 +332,8 @@ function downloadSummaryPdf() {
} }
function adminStartEdit(email) { function adminStartEdit(email) {
fetch(ADMIN_URL + '?week=' + encodeURIComponent(currentAdminWeek), { headers: adminHeaders() }) setAdminActionDisabled('start-edit', true);
adminFetch(ADMIN_URL + '?week=' + encodeURIComponent(currentAdminWeek), { headers: adminHeaders() })
.then(r => { .then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status); if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json(); return r.json();
@ -324,7 +347,8 @@ function adminStartEdit(email) {
order.items.forEach(i => { editQuantities[i.name] = i.quantity; }); order.items.forEach(i => { editQuantities[i.name] = i.quantity; });
renderAdminTable(data); renderAdminTable(data);
}) })
.catch(() => alert('Failed to load this order for editing.')); .catch(() => alert('Failed to load this order for editing.'))
.finally(() => setAdminActionDisabled('start-edit', false));
} }
function adminCancelEdit() { function adminCancelEdit() {
@ -355,26 +379,30 @@ function adminSaveEdit(email) {
} }
return; return;
} }
fetch(ADMIN_URL, { setAdminActionDisabled('save-edit', true);
adminFetch(ADMIN_URL, {
method: 'PUT', method: 'PUT',
headers: { ...adminHeaders(), 'Content-Type': 'application/json' }, headers: { ...adminHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({ week: currentAdminWeek, email, items }), body: JSON.stringify({ week: currentAdminWeek, email, items }),
}) })
.then(r => { if (!r.ok) return r.json().then(d => { throw new Error(d.error); }); return r.json(); }) .then(r => { if (!r.ok) return r.json().then(d => { throw new Error(d.error); }); return r.json(); })
.then(() => { editingOrder = null; editQuantities = {}; loadWeekOrders(currentAdminWeek); }) .then(() => { editingOrder = null; editQuantities = {}; loadWeekOrders(currentAdminWeek); })
.catch(e => alert('Failed to update: ' + e.message)); .catch(e => alert('Failed to update: ' + e.message))
.finally(() => setAdminActionDisabled('save-edit', false));
} }
function adminDeleteOrder(email, name) { function adminDeleteOrder(email, name) {
const label = name ? name + ' (' + email + ')' : email; const label = name ? name + ' (' + email + ')' : email;
if (!confirm('Delete order for ' + label + '?')) return; if (!confirm('Delete order for ' + label + '?')) return;
fetch(ADMIN_URL + '?week=' + encodeURIComponent(currentAdminWeek) + '&email=' + encodeURIComponent(email), { setAdminActionDisabled('delete', true);
adminFetch(ADMIN_URL + '?week=' + encodeURIComponent(currentAdminWeek) + '&email=' + encodeURIComponent(email), {
method: 'DELETE', method: 'DELETE',
headers: adminHeaders(), headers: adminHeaders(),
}) })
.then(r => { if (!r.ok) return r.json().then(d => { throw new Error(d.error); }); return r.json(); }) .then(r => { if (!r.ok) return r.json().then(d => { throw new Error(d.error); }); return r.json(); })
.then(() => loadWeekOrders(currentAdminWeek)) .then(() => loadWeekOrders(currentAdminWeek))
.catch(e => alert('Failed to delete: ' + e.message)); .catch(e => alert('Failed to delete: ' + e.message))
.finally(() => setAdminActionDisabled('delete', false));
} }
function handleAdminPanelClick(e) { function handleAdminPanelClick(e) {

View file

@ -6,6 +6,7 @@
--radius-card: 12px; --radius-card: 12px;
--radius-control: 8px; --radius-control: 8px;
--shadow-card: 0 1px 3px rgba(0,0,0,0.08); --shadow-card: 0 1px 3px rgba(0,0,0,0.08);
--sticky-footer-height: 80px;
} }
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
@ -14,7 +15,7 @@ body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f7; background: #f5f5f7;
color: #1d1d1f; color: #1d1d1f;
padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px)); padding-bottom: calc(var(--sticky-footer-height) + env(safe-area-inset-bottom, 0px));
} }
.is-hidden { display: none !important; } .is-hidden { display: none !important; }
@ -194,6 +195,7 @@ header p { font-size: 0.85rem; opacity: 0.8; }
background: #fff; background: #fff;
border-top: 1px solid #e5e7eb; border-top: 1px solid #e5e7eb;
padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px)); padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
min-height: calc(var(--sticky-footer-height) + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -2px 8px rgba(0,0,0,0.1); box-shadow: 0 -2px 8px rgba(0,0,0,0.1);
z-index: 100; z-index: 100;
} }

View file

@ -70,7 +70,7 @@
</div> </div>
{% endif %} {% endif %}
<input type="text" class="search-bar" id="search" placeholder="Search meals..."> <input type="text" class="search-bar" id="search" placeholder="Search meals..." aria-label="Search meals">
<div class="filters" id="filters"></div> <div class="filters" id="filters"></div>

View file

@ -29,6 +29,8 @@ let reopenAtMs = null;
let countdownTimer = null; let countdownTimer = null;
let mealsListDelegationBound = false; let mealsListDelegationBound = false;
const GOOGLE_AUTH_MAX_ATTEMPTS = 200;
window.googleCredential = null; window.googleCredential = null;
function employeePrice(retail) { function employeePrice(retail) {
@ -52,11 +54,17 @@ function buildItems() {
})); }));
} }
function waitForGoogleAuth() { function waitForGoogleAuth(attempt = 0) {
if (typeof google !== 'undefined' && google.accounts && google.accounts.id) { if (typeof google !== 'undefined' && google.accounts && google.accounts.id) {
initGoogleAuth(); initGoogleAuth();
} else if (attempt < GOOGLE_AUTH_MAX_ATTEMPTS) {
setTimeout(() => waitForGoogleAuth(attempt + 1), 50);
} else { } else {
setTimeout(waitForGoogleAuth, 50); const signIn = document.getElementById('g-signin-btn');
if (signIn) {
signIn.textContent = 'Google sign-in failed to load. Refresh the page to try again.';
}
console.error('Google Identity Services failed to load.');
} }
} }
@ -253,10 +261,14 @@ function updateMealCardQty(index) {
if (qtyInput && qtyInput !== document.activeElement) qtyInput.value = qty; if (qtyInput && qtyInput !== document.activeElement) qtyInput.value = qty;
} }
function setQty(index, value) { function setQtyState(index, value) {
const next = Math.max(0, parseInt(value, 10) || 0); const next = Math.max(0, parseInt(value, 10) || 0);
if (next === 0) delete quantities[index]; if (next === 0) delete quantities[index];
else quantities[index] = next; else quantities[index] = next;
}
function setQty(index, value) {
setQtyState(index, value);
updateMealCardQty(index); updateMealCardQty(index);
updateTotal(); updateTotal();
} }
@ -285,7 +297,8 @@ function bindMealsListDelegation() {
const card = input.closest('[data-meal-index]'); const card = input.closest('[data-meal-index]');
if (!card) return; if (!card) return;
const index = parseInt(card.dataset.mealIndex, 10); const index = parseInt(card.dataset.mealIndex, 10);
setQty(index, input.value); setQtyState(index, input.value);
updateTotal();
}); });
list.addEventListener('focusout', (e) => { list.addEventListener('focusout', (e) => {
const input = e.target.closest('.qty'); const input = e.target.closest('.qty');

View file

@ -168,10 +168,18 @@ class TestGenerateFormStructural:
'content="width=device-width, initial-scale=1.0, viewport-fit=cover"' 'content="width=device-width, initial-scale=1.0, viewport-fit=cover"'
in html in html
) )
assert "padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));" in css assert "--sticky-footer-height: 80px;" in css
assert (
"padding-bottom: calc(var(--sticky-footer-height) + "
"env(safe-area-inset-bottom, 0px));"
) in css
assert ( assert (
"padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));" "padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));"
) in css ) in css
assert (
"min-height: calc(var(--sticky-footer-height) + "
"env(safe-area-inset-bottom, 0px));"
) in css
def test_script_order_form_then_admin(self): def test_script_order_form_then_admin(self):
html = _render() html = _render()