fix(form): address admin and form review findings

This commit is contained in:
Adam Moussa 2026-07-30 18:41:02 -04:00
parent 2f88a4a315
commit e00d74ebad
No known key found for this signature in database
3 changed files with 26 additions and 22 deletions

View file

@ -28,7 +28,10 @@ function loadWeekOrders(week) {
document.getElementById('admin-stats').textContent = ''; document.getElementById('admin-stats').textContent = '';
fetch(ADMIN_URL + '?week=' + encodeURIComponent(week), { headers: adminHeaders() }) fetch(ADMIN_URL + '?week=' + encodeURIComponent(week), { headers: adminHeaders() })
.then(r => r.json()) .then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(data => renderAdminTable(data)) .then(data => renderAdminTable(data))
.catch(() => { .catch(() => {
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>';
@ -187,7 +190,7 @@ function renderAdminTable(data) {
MEALS.forEach(m => { menuPrices[m.name] = m.price; }); MEALS.forEach(m => { menuPrices[m.name] = m.price; });
const newTotal = editNames.reduce((sum, name) => { const newTotal = editNames.reduce((sum, name) => {
const price = menuPrices[name] || 0; const price = menuPrices[name] || 0;
return sum + price * (editQuantities[name] || 0); return sum + window.FormUtils.employeePrice(price) * (editQuantities[name] || 0);
}, 0); }, 0);
totalHtml = '$' + newTotal.toFixed(2); totalHtml = '$' + newTotal.toFixed(2);
tr.appendChild(empTd); tr.appendChild(empTd);

View file

@ -26,6 +26,7 @@ let adminWeeks = [];
let googleCredential = null; let googleCredential = null;
let googleUser = null; let googleUser = null;
let reopenAtMs = null; let reopenAtMs = null;
let countdownTimer = null;
let mealsListDelegationBound = false; let mealsListDelegationBound = false;
window.googleCredential = null; window.googleCredential = null;
@ -103,7 +104,9 @@ function handleCredentialResponse(response) {
updateTotal(); updateTotal();
checkDuplicateOrder(); checkDuplicateOrder();
checkAdmin(); checkAdmin().then(() => {
if (formClosed && !isAdmin) showClosedOverlay();
});
} }
function signOut() { function signOut() {
@ -257,7 +260,7 @@ function updateMealCardQty(index) {
const qty = quantities[index] || 0; const qty = quantities[index] || 0;
card.classList.toggle('selected', qty > 0); card.classList.toggle('selected', qty > 0);
const qtyInput = card.querySelector('.qty'); const qtyInput = card.querySelector('.qty');
if (qtyInput) qtyInput.value = qty; if (qtyInput && qtyInput !== document.activeElement) qtyInput.value = qty;
} }
function setQty(index, value) { function setQty(index, value) {
@ -286,14 +289,6 @@ function bindMealsListDelegation() {
const delta = parseInt(btn.dataset.qtyDelta, 10); const delta = parseInt(btn.dataset.qtyDelta, 10);
changeQty(index, delta); changeQty(index, delta);
}); });
list.addEventListener('change', (e) => {
const input = e.target.closest('.qty');
if (!input) return;
const card = input.closest('[data-meal-index]');
if (!card) return;
const index = parseInt(card.dataset.mealIndex, 10);
setQty(index, input.value);
});
list.addEventListener('input', (e) => { list.addEventListener('input', (e) => {
const input = e.target.closest('.qty'); const input = e.target.closest('.qty');
if (!input) return; if (!input) return;
@ -302,6 +297,13 @@ function bindMealsListDelegation() {
const index = parseInt(card.dataset.mealIndex, 10); const index = parseInt(card.dataset.mealIndex, 10);
setQty(index, input.value); setQty(index, input.value);
}); });
list.addEventListener('focusout', (e) => {
const input = e.target.closest('.qty');
if (!input) return;
const card = input.closest('[data-meal-index]');
if (!card) return;
updateMealCardQty(parseInt(card.dataset.mealIndex, 10));
});
} }
function updateTotal() { function updateTotal() {
@ -405,7 +407,7 @@ function showClosedOverlay() {
const overlay = document.getElementById('closed-overlay'); const overlay = document.getElementById('closed-overlay');
overlay.classList.add('is-visible'); overlay.classList.add('is-visible');
overlay.style.display = 'flex'; overlay.style.display = 'flex';
document.querySelectorAll('.qty-control button').forEach(b => { b.disabled = true; }); document.querySelectorAll('.qty-control button, .qty-control .qty').forEach(el => { el.disabled = true; });
const submitBtn = document.getElementById('submit-btn'); const submitBtn = document.getElementById('submit-btn');
if (submitBtn) submitBtn.disabled = true; if (submitBtn) submitBtn.disabled = true;
if (formStatusData) startCountdown(formStatusData); if (formStatusData) startCountdown(formStatusData);
@ -435,10 +437,10 @@ function adminBypassClosed() {
} }
function checkAdmin() { function checkAdmin() {
if (!ADMIN_URL) return; if (!ADMIN_URL) return Promise.resolve();
const headers = {}; const headers = {};
if (googleCredential) headers['Authorization'] = 'Bearer ' + googleCredential; if (googleCredential) headers['Authorization'] = 'Bearer ' + googleCredential;
fetch(ADMIN_URL, { headers }) return fetch(ADMIN_URL, { headers })
.then(r => r.ok ? r.json() : null) .then(r => r.ok ? r.json() : null)
.then(data => { .then(data => {
if (data && data.weeks) { if (data && data.weeks) {
@ -520,7 +522,8 @@ function getEasternOffsetMs(date) {
function startCountdown(data) { function startCountdown(data) {
if (data && data.reopen_at) reopenAtMs = data.reopen_at * 1000; if (data && data.reopen_at) reopenAtMs = data.reopen_at * 1000;
updateCountdown(); updateCountdown();
setInterval(updateCountdown, 1000); if (countdownTimer !== null) clearInterval(countdownTimer);
countdownTimer = setInterval(updateCountdown, 1000);
} }
function updateCountdown() { function updateCountdown() {
@ -561,8 +564,7 @@ async function checkFormStatus() {
formStatusData = data; formStatusData = data;
if (isAdmin) { if (isAdmin) {
adminBypassClosed(); adminBypassClosed();
} else if (CONFIG.authMode === 'google') { } else if (CONFIG.authMode !== 'google' || googleCredential) {
} else {
showClosedOverlay(); showClosedOverlay();
} }
} }

View file

@ -150,7 +150,6 @@ class TestGenerateFormStructural:
blob = cfg_match.group(1) blob = cfg_match.group(1)
# Literal </script> inside the CONFIG script would terminate early. # Literal </script> inside the CONFIG script would terminate early.
assert "</script>" not in blob assert "</script>" not in blob
assert "<\\/script><script>alert(1)<\\/script>" in blob
# Parsed JSON still contains the hostile meal name. # Parsed JSON still contains the hostile meal name.
cfg = json.loads(blob) cfg = json.loads(blob)
assert any( assert any(
@ -281,7 +280,7 @@ class TestGenerateFormPlaywright:
# Reset first card via type-in # Reset first card via type-in
qty = page.locator('[data-meal-index="0"] .qty') qty = page.locator('[data-meal-index="0"] .qty')
qty.fill("-3") qty.fill("-3")
qty.dispatch_event("change") qty.blur()
page.wait_for_function( page.wait_for_function(
"() => document.querySelector('[data-meal-index=\"0\"] .qty').value === '0'" "() => document.querySelector('[data-meal-index=\"0\"] .qty').value === '0'"
) )
@ -292,7 +291,7 @@ class TestGenerateFormPlaywright:
"""() => { """() => {
document.querySelectorAll('[data-meal-index] .qty').forEach((input) => { document.querySelectorAll('[data-meal-index] .qty').forEach((input) => {
input.value = '0'; input.value = '0';
input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('input', { bubbles: true }));
}); });
}""" }"""
) )
@ -300,7 +299,7 @@ class TestGenerateFormPlaywright:
"() => document.getElementById('count-display').textContent === '0 meals'" "() => document.getElementById('count-display').textContent === '0 meals'"
) )
qty.fill("2") qty.fill("2")
qty.dispatch_event("change") qty.blur()
page.wait_for_function( page.wait_for_function(
"() => document.getElementById('count-display').textContent === '2 meals'" "() => document.getElementById('count-display').textContent === '2 meals'"
) )