mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-10-07 16:18:54 +00:00
fix(form): address review findings
This commit is contained in:
parent
dea398626f
commit
e60b8e26dd
6 changed files with 72 additions and 21 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(() => {
|
||||||
tbody.innerHTML = '<tr><td colspan="5" class="admin-error">Failed to load orders.</td></tr>';
|
if (requestId === latestAdminRequest && week === currentAdminWeek) {
|
||||||
|
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) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue