/* ADMIN_JS_MARKER */ (() => { const ADMIN_URL = window.ADMIN_URL; const ADMIN_PDF_URL = window.ADMIN_PDF_URL; const MEALS = window.MEALS; const WEEK = window.WEEK; const ADMIN_REQUEST_TIMEOUT_MS = 10000; let currentAdminWeek = ""; let editingOrder = null; let editQuantities = {}; let lastAdminData = null; let latestAdminRequest = 0; function adminHeaders() { const h = {}; const cred = window.googleCredential; if (cred) h["Authorization"] = "Bearer " + cred; 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) { if (!week) return; const requestId = ++latestAdminRequest; window.FormUtils.clearStatus("admin"); currentAdminWeek = week; editingOrder = null; editQuantities = {}; const tbody = document.getElementById("admin-tbody"); tbody.innerHTML = 'Loading...'; const cards = document.getElementById("admin-cards"); cards.innerHTML = '
Loading...
'; document.getElementById("admin-empty").classList.add("is-hidden"); document.getElementById("admin-stats").textContent = ""; adminFetch(ADMIN_URL + "?week=" + encodeURIComponent(week), { headers: adminHeaders(), }) .then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then((data) => { if (requestId === latestAdminRequest && week === currentAdminWeek) { renderAdminTable(data); } }) .catch(() => { if (requestId === latestAdminRequest && week === currentAdminWeek) { tbody.innerHTML = 'Failed to load orders.'; cards.innerHTML = ""; window.FormUtils.showStatus("Failed to load orders.", { tone: "error", scope: "admin", }); } }); } window.loadWeekOrders = loadWeekOrders; function buildEditQtyRow(name, qty) { const wrap = document.createElement("div"); wrap.className = qty === 0 ? "edit-qty removed" : "edit-qty"; const minusBtn = document.createElement("button"); minusBtn.type = "button"; 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); wrap.appendChild(span); wrap.appendChild(plusBtn); wrap.appendChild(document.createTextNode(" " + name)); return wrap; } function buildAddItemSelect(availableMeals) { const wrap = document.createElement("div"); wrap.className = "admin-add-item"; const select = document.createElement("select"); select.dataset.adminAction = "add-item-select"; const defaultOpt = document.createElement("option"); defaultOpt.value = ""; defaultOpt.textContent = "+ Add item..."; select.appendChild(defaultOpt); availableMeals.forEach((m) => { const opt = document.createElement("option"); const price = m.price || 0; opt.value = m.name; opt.textContent = m.name + " ($" + price.toFixed(2) + ")"; select.appendChild(opt); }); wrap.appendChild(select); return wrap; } function buildActionButton(label, action, extra) { const btn = document.createElement("button"); btn.type = "button"; btn.dataset.adminAction = action; if (extra) { Object.entries(extra).forEach(([key, val]) => { btn.dataset[key] = val; }); } btn.textContent = label; return btn; } function buildEditActions(email) { const wrap = document.createElement("div"); wrap.className = "admin-actions"; const saveBtn = buildActionButton("Save", "save-edit", { email }); saveBtn.className = "btn btn-sm btn-save"; const cancelBtn = buildActionButton("Cancel", "cancel-edit"); cancelBtn.className = "btn btn-sm btn-cancel"; wrap.appendChild(saveBtn); wrap.appendChild(cancelBtn); return wrap; } function buildViewActions(email, name) { const wrap = document.createElement("div"); wrap.className = "admin-actions"; const editBtn = buildActionButton("Edit", "start-edit", { email }); editBtn.className = "btn btn-sm btn-edit"; const deleteBtn = buildActionButton("Delete", "delete", { email, name }); deleteBtn.className = "btn btn-sm btn-delete"; wrap.appendChild(editBtn); wrap.appendChild(deleteBtn); return wrap; } function formatSubmittedAt(submittedAt) { return submittedAt ? new Date(submittedAt).toLocaleString("en-US", { timeZone: "America/New_York", month: "short", day: "numeric", hour: "numeric", minute: "2-digit", }) : ""; } function editTotal() { const menuPrices = {}; MEALS.forEach((m) => { menuPrices[m.name] = m.price; }); return Object.keys(editQuantities).reduce((sum, name) => { const price = menuPrices[name] || 0; return ( sum + window.FormUtils.employeePrice(price) * (editQuantities[name] || 0) ); }, 0); } function populateOrderItems(container, order, isEditing, separatorFactory) { if (isEditing) { Object.keys(editQuantities).forEach((name) => { container.appendChild(buildEditQtyRow(name, editQuantities[name] || 0)); if (separatorFactory) container.appendChild(separatorFactory()); }); const availableMeals = MEALS.filter( (m) => !Object.prototype.hasOwnProperty.call(editQuantities, m.name), ); if (availableMeals.length > 0) { container.appendChild(buildAddItemSelect(availableMeals)); } return "$" + editTotal().toFixed(2); } order.items.forEach((item, itemIdx) => { if (separatorFactory && itemIdx > 0) { container.appendChild(separatorFactory()); } const row = separatorFactory ? container : document.createElement("div"); if (!separatorFactory) row.className = "admin-item"; row.appendChild( document.createTextNode(item.name + " ×" + item.quantity + " "), ); const sub = document.createElement("span"); sub.className = "admin-muted"; sub.textContent = "($" + item.subtotal.toFixed(2) + ")"; row.appendChild(sub); if (!separatorFactory) container.appendChild(row); }); return "$" + order.total.toFixed(2); } function buildAdminCard(order) { const card = document.createElement("article"); card.className = "admin-order-card"; card.dataset.email = order.employee_email; const isEditing = editingOrder === order.employee_email; const header = document.createElement("div"); header.className = "admin-card-header"; const employee = document.createElement("div"); employee.className = "admin-card-employee"; const name = document.createElement("strong"); name.textContent = order.employee_name; const email = document.createElement("span"); email.className = "admin-muted"; email.textContent = order.employee_email; employee.appendChild(name); employee.appendChild(email); const time = document.createElement("span"); time.className = "admin-card-time"; time.textContent = formatSubmittedAt(order.submitted_at); header.appendChild(employee); header.appendChild(time); const items = document.createElement("div"); items.className = "admin-card-items"; const total = populateOrderItems(items, order, isEditing); const footer = document.createElement("div"); footer.className = "admin-card-footer"; const totalWrap = document.createElement("div"); totalWrap.className = "admin-card-total"; const totalLabel = document.createElement("span"); totalLabel.textContent = "Total"; const totalValue = document.createElement("strong"); totalValue.textContent = total; totalWrap.appendChild(totalLabel); totalWrap.appendChild(totalValue); footer.appendChild(totalWrap); footer.appendChild( isEditing ? buildEditActions(order.employee_email) : buildViewActions(order.employee_email, order.employee_name), ); card.appendChild(header); card.appendChild(items); card.appendChild(footer); return card; } function renderAdminCards(data) { const cards = document.getElementById("admin-cards"); cards.innerHTML = ""; (data.orders || []).forEach((order) => { cards.appendChild(buildAdminCard(order)); }); } function renderAdminTable(data) { lastAdminData = data; const hasOrders = !!(data.orders && data.orders.length > 0); const dlBtn = document.getElementById("admin-download-btn"); if (dlBtn) dlBtn.disabled = !hasOrders; const tbody = document.getElementById("admin-tbody"); tbody.innerHTML = ""; const cards = document.getElementById("admin-cards"); cards.innerHTML = ""; if (!hasOrders) { document.getElementById("admin-empty").classList.remove("is-hidden"); document.getElementById("admin-stats").textContent = ""; return; } document.getElementById("admin-empty").classList.add("is-hidden"); const statsEl = document.getElementById("admin-stats"); statsEl.replaceChildren(); const empStrong = document.createElement("strong"); empStrong.textContent = String(data.total_employees); statsEl.appendChild(empStrong); statsEl.appendChild( document.createTextNode( " employee" + (data.total_employees !== 1 ? "s" : "") + " ordered · ", ), ); const totalStrong = document.createElement("strong"); totalStrong.textContent = "$" + data.grand_total.toFixed(2); statsEl.appendChild(totalStrong); statsEl.appendChild(document.createTextNode(" total payroll deductions")); data.orders.forEach((order, idx) => { const tr = document.createElement("tr"); tr.id = "admin-row-" + idx; const isEditing = editingOrder === order.employee_email; const empTd = document.createElement("td"); const nameStrong = document.createElement("strong"); nameStrong.textContent = order.employee_name; empTd.appendChild(nameStrong); empTd.appendChild(document.createElement("br")); const emailSpan = document.createElement("span"); emailSpan.className = "admin-muted"; emailSpan.textContent = order.employee_email; empTd.appendChild(emailSpan); const itemsTd = document.createElement("td"); itemsTd.className = "items-cell"; const totalHtml = populateOrderItems(itemsTd, order, isEditing, () => document.createElement("br"), ); tr.appendChild(empTd); tr.appendChild(itemsTd); const totalTd = document.createElement("td"); totalTd.className = "total-cell"; totalTd.textContent = totalHtml; const timeTd = document.createElement("td"); timeTd.className = "time-cell"; timeTd.textContent = formatSubmittedAt(order.submitted_at); const actionsTd = document.createElement("td"); if (isEditing) { actionsTd.appendChild(buildEditActions(order.employee_email)); } else { actionsTd.appendChild( buildViewActions(order.employee_email, order.employee_name), ); } tr.appendChild(totalTd); tr.appendChild(timeTd); tr.appendChild(actionsTd); tbody.appendChild(tr); }); const totalRow = document.createElement("tr"); totalRow.className = "admin-total-row"; const mealCount = data.orders.reduce( (s, o) => s + o.items.reduce((a, i) => a + i.quantity, 0), 0, ); const t1 = document.createElement("td"); t1.textContent = "Total"; const t2 = document.createElement("td"); t2.textContent = mealCount + (mealCount === 1 ? " meal" : " meals"); const t3 = document.createElement("td"); t3.className = "total-cell"; t3.textContent = "$" + data.grand_total.toFixed(2); totalRow.appendChild(t1); totalRow.appendChild(t2); totalRow.appendChild(t3); totalRow.appendChild(document.createElement("td")); totalRow.appendChild(document.createElement("td")); tbody.appendChild(totalRow); renderAdminCards(data); } function downloadOrderList() { const data = lastAdminData; if (!data || !data.orders || data.orders.length === 0) return; const totals = {}; data.orders.forEach((o) => (o.items || []).forEach((i) => { const name = i.name || ""; totals[name] = (totals[name] || 0) + (i.quantity || 0); }), ); const names = Object.keys(totals).sort((a, b) => a.localeCompare(b)); const esc = (v) => { const s = String(v); const safe = /^[=+\-@\t\r]/.test(s) ? "'" + s : s; return /[",\n]/.test(safe) ? '"' + safe.replace(/"/g, '""') + '"' : safe; }; const rows = [["Item", "Quantity"]].concat( names.map((n) => [n, totals[n]]), ); const csv = rows.map((r) => r.map(esc).join(",")).join("\r\n"); const week = data.week || currentAdminWeek || WEEK; const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "order-list-" + week + ".csv"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } 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 }) => { if (ok && data.url) { // Only follow https presigned URLs from our admin API. if (typeof data.url === "string" && data.url.startsWith("https://")) { window.location.href = data.url; } else { window.FormUtils.showStatus( "Could not fetch the summary PDF: invalid URL", { tone: "error", scope: "admin", }, ); } } else if (status === 404) { window.FormUtils.showStatus( "No summary PDF for " + week + " yet — it is generated when the week closes on Thursday.", { tone: "info", scope: "admin" }, ); } else { window.FormUtils.showStatus( "Could not fetch the summary PDF: " + (data.error || "unknown error"), { tone: "error", scope: "admin" }, ); } }) .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(), }) .then((r) => { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then((data) => { if (currentAdminWeek !== requestedWeek) return; lastAdminData = data; const order = data.orders.find((o) => o.employee_email === email); if (!order) return; editingOrder = email; editQuantities = {}; order.items.forEach((i) => { editQuantities[i.name] = i.quantity; }); renderAdminTable(data); }) .catch(() => { window.FormUtils.showStatus("Failed to load this order for editing.", { tone: "error", scope: "admin", }); }) .finally(() => setAdminActionDisabled("start-edit", false)); } function adminCancelEdit() { editingOrder = null; editQuantities = {}; if (lastAdminData) renderAdminTable(lastAdminData); } function adminEditQty(name, delta) { const current = editQuantities[name] || 0; editQuantities[name] = Math.max(0, current + delta); if (lastAdminData) renderAdminTable(lastAdminData); } function adminAddItem(name) { if (!name) return; editQuantities[name] = 1; if (lastAdminData) renderAdminTable(lastAdminData); } function adminSaveEdit(email) { const items = Object.entries(editQuantities) .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", headers: { ...adminHeaders(), "Content-Type": "application/json" }, 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(() => { editingOrder = null; editQuantities = {}; loadWeekOrders(currentAdminWeek); }) .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", headers: adminHeaders(), }, ) .then((r) => { if (!r.ok) return r.json().then((d) => { throw new Error(d.error); }); return r.json(); }) .then(() => loadWeekOrders(currentAdminWeek)) .catch((e) => { window.FormUtils.showStatus("Failed to delete: " + e.message, { tone: "error", scope: "admin", }); }) .finally(() => setAdminActionDisabled("delete", false)); } function handleAdminPanelClick(e) { const btn = e.target.closest("[data-admin-action]"); if (btn) { const action = btn.dataset.adminAction; if (action === "start-edit") { adminStartEdit(btn.dataset.email); } else if (action === "delete") { adminDeleteOrder(btn.dataset.email, btn.dataset.name || ""); } else if (action === "save-edit") { adminSaveEdit(btn.dataset.email); } else if (action === "cancel-edit") { adminCancelEdit(); } else if (action === "edit-qty") { adminEditQty(btn.dataset.mealName, parseInt(btn.dataset.delta, 10)); } else if (action === "download-list") { downloadOrderList(); } else if (action === "download-pdf") { downloadSummaryPdf(); } else if (action === "hide-admin") { window.hideAdmin(); } return; } } function adminInit() { const panel = document.getElementById("admin-panel"); if (panel) { panel.addEventListener("click", handleAdminPanelClick); panel.addEventListener("change", (e) => { const select = e.target.closest( '[data-admin-action="add-item-select"]', ); if (select && select.value) { adminAddItem(select.value); select.selectedIndex = 0; } }); } const weekSelect = document.getElementById("admin-week"); if (weekSelect) { weekSelect.addEventListener("change", () => loadWeekOrders(weekSelect.value), ); } } adminInit(); })();