mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-10-02 22:43:15 +00:00
Update order form UI and CI workflow for new features
Form now shows discount pricing, responsive grid layout, Google Sign-In overlay, and closed-orders page with countdown timer. CI workflow fetches discount settings from DynamoDB and Google Client ID from SSM.
This commit is contained in:
parent
ef4726aa3f
commit
f437ca8f0f
2 changed files with 347 additions and 67 deletions
37
.github/workflows/weekly-menu.yml
vendored
37
.github/workflows/weekly-menu.yml
vendored
|
|
@ -72,11 +72,46 @@ jobs:
|
|||
echo "::add-mask::$API_KEY"
|
||||
echo "api_key=$API_KEY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get discount settings
|
||||
id: discount
|
||||
run: |
|
||||
RESULT=$(aws dynamodb get-item \
|
||||
--table-name meal-order-manager-orders \
|
||||
--key '{"PK":{"S":"CONFIG"},"SK":{"S":"SETTINGS"}}' \
|
||||
--output json 2>/dev/null || echo '{}')
|
||||
BULK=$(echo "$RESULT" | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
print(d.get('Item',{}).get('bulk_discount_percent',{}).get('N','0'))
|
||||
" 2>/dev/null || echo "0")
|
||||
SUBSIDY=$(echo "$RESULT" | python3 -c "
|
||||
import sys, json
|
||||
d = json.load(sys.stdin)
|
||||
print(d.get('Item',{}).get('company_subsidy_percent',{}).get('N','0'))
|
||||
" 2>/dev/null || echo "0")
|
||||
echo "bulk_discount=$BULK" >> $GITHUB_OUTPUT
|
||||
echo "company_subsidy=$SUBSIDY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get Google Client ID
|
||||
id: google
|
||||
run: |
|
||||
GOOGLE_CLIENT_ID=$(aws ssm get-parameter \
|
||||
--name /meal-order-manager/google-client-id \
|
||||
--query 'Parameter.Value' \
|
||||
--output text 2>/dev/null || echo "")
|
||||
if [ "$GOOGLE_CLIENT_ID" = "None" ] || [ -z "$GOOGLE_CLIENT_ID" ]; then
|
||||
GOOGLE_CLIENT_ID=""
|
||||
fi
|
||||
echo "client_id=$GOOGLE_CLIENT_ID" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate order form
|
||||
run: |
|
||||
python3 src/server/generate_form.py \
|
||||
--api-url "${{ steps.stack.outputs.api_url }}" \
|
||||
--api-key "${{ steps.apikey.outputs.api_key }}"
|
||||
--api-key "${{ steps.apikey.outputs.api_key }}" \
|
||||
--bulk-discount "${{ steps.discount.outputs.bulk_discount }}" \
|
||||
--company-subsidy "${{ steps.discount.outputs.company_subsidy }}" \
|
||||
${{ steps.google.outputs.client_id && format('--google-client-id "{0}"', steps.google.outputs.client_id) || '' }}
|
||||
|
||||
- name: Upload menu to DynamoDB
|
||||
run: python3 scripts/upload_menu.py
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import sys
|
|||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import boto3
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
OUTPUT_DIR = PROJECT_ROOT / "output"
|
||||
CONFIG_PATH = PROJECT_ROOT / "config.json"
|
||||
|
|
@ -35,7 +37,13 @@ def latest_menu() -> dict:
|
|||
|
||||
|
||||
def generate_form(
|
||||
menu: dict, config: dict, api_url: str = "", api_key: str = ""
|
||||
menu: dict,
|
||||
config: dict,
|
||||
api_url: str = "",
|
||||
api_key: str = "",
|
||||
bulk_discount: float = 0,
|
||||
company_subsidy: float = 0,
|
||||
google_client_id: str = "",
|
||||
) -> str:
|
||||
meals_json = json.dumps(menu["meals"])
|
||||
roster_json = json.dumps(config.get("roster", []))
|
||||
|
|
@ -43,9 +51,174 @@ def generate_form(
|
|||
week = datetime.now().strftime("%Y-W%U")
|
||||
scraped_at = menu.get("scraped_at", "unknown")
|
||||
submit_url = f"{api_url}/api/submit-order" if api_url else "/api/submit-order"
|
||||
status_url = f"{api_url}/api/form-status/{week}" if api_url else ""
|
||||
status_url = f"{api_url}/api/form-status/{week}" if api_url else f"/api/form-status/{week}"
|
||||
roster_url = f"{api_url}/api/roster" if api_url else "/api/roster"
|
||||
api_key_json = json.dumps(api_key)
|
||||
has_discount = bulk_discount > 0 or company_subsidy > 0
|
||||
use_google_auth = bool(google_client_id)
|
||||
google_client_id_json = json.dumps(google_client_id)
|
||||
|
||||
if use_google_auth:
|
||||
auth_section_html = """ <div class="employee-info">
|
||||
<div id="user-info">
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<img id="user-avatar" style="width:40px;height:40px;border-radius:50%;object-fit:cover;display:none;" alt="">
|
||||
<div style="flex:1;">
|
||||
<div id="user-name" style="font-weight:600;font-size:0.95rem;"></div>
|
||||
<div id="user-email" style="font-size:0.85rem;color:#6b7280;"></div>
|
||||
</div>
|
||||
<button onclick="signOut()" style="background:none;border:1px solid #d1d5db;border-radius:8px;padding:6px 14px;cursor:pointer;font-size:0.8rem;color:#6b7280;">Sign out</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>"""
|
||||
else:
|
||||
auth_section_html = """ <div class="employee-info">
|
||||
<label for="emp-name">Your Name</label>
|
||||
<select id="emp-name"><option value="">Loading...</option></select>
|
||||
<input type="hidden" id="emp-email">
|
||||
</div>"""
|
||||
|
||||
if use_google_auth:
|
||||
google_auth_js = """
|
||||
function waitForGoogleAuth() {
|
||||
if (typeof google !== 'undefined' && google.accounts && google.accounts.id) {
|
||||
initGoogleAuth();
|
||||
} else {
|
||||
setTimeout(waitForGoogleAuth, 50);
|
||||
}
|
||||
}
|
||||
|
||||
function initGoogleAuth() {
|
||||
google.accounts.id.initialize({
|
||||
client_id: GOOGLE_CLIENT_ID,
|
||||
callback: handleCredentialResponse,
|
||||
hosted_domain: 'seahavenind.com',
|
||||
auto_select: true,
|
||||
});
|
||||
google.accounts.id.renderButton(
|
||||
document.getElementById('g-signin-btn'),
|
||||
{ theme: 'outline', size: 'large', text: 'signin_with', width: 300 }
|
||||
);
|
||||
}
|
||||
|
||||
function handleCredentialResponse(response) {
|
||||
googleCredential = response.credential;
|
||||
const payload = JSON.parse(atob(response.credential.split('.')[1]));
|
||||
googleUser = { name: payload.name, email: payload.email };
|
||||
|
||||
document.getElementById('auth-overlay').style.display = 'none';
|
||||
document.getElementById('app').style.display = 'block';
|
||||
var footer = document.getElementById('sticky-footer');
|
||||
if (footer) footer.style.display = 'block';
|
||||
document.getElementById('user-name').textContent = payload.name;
|
||||
document.getElementById('user-email').textContent = payload.email;
|
||||
if (payload.picture) {
|
||||
const avatar = document.getElementById('user-avatar');
|
||||
avatar.src = payload.picture;
|
||||
avatar.style.display = 'block';
|
||||
}
|
||||
|
||||
updateTotal();
|
||||
}
|
||||
|
||||
function signOut() {
|
||||
googleCredential = null;
|
||||
googleUser = null;
|
||||
google.accounts.id.disableAutoSelect();
|
||||
document.getElementById('auth-overlay').style.display = 'flex';
|
||||
document.getElementById('app').style.display = 'none';
|
||||
var footer = document.getElementById('sticky-footer');
|
||||
if (footer) footer.style.display = 'none';
|
||||
}
|
||||
"""
|
||||
else:
|
||||
google_auth_js = ""
|
||||
|
||||
if use_google_auth:
|
||||
submit_order_js = """
|
||||
async function submitOrder() {
|
||||
if (!googleCredential || !googleUser) { alert('Please sign in with Google first.'); return; }
|
||||
|
||||
const items = Object.entries(quantities).map(([i, qty]) => ({
|
||||
name: MEALS[i].name,
|
||||
retail_price: MEALS[i].price,
|
||||
quantity: qty,
|
||||
}));
|
||||
|
||||
const btn = document.getElementById('submit-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Submitting...';
|
||||
|
||||
try {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (API_KEY) headers['x-api-key'] = API_KEY;
|
||||
const res = await fetch(SUBMIT_URL, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ google_id_token: googleCredential, items }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
for (const el of document.getElementById('app').children) { if (el.id !== 'success') el.style.display = 'none'; }
|
||||
document.getElementById('success').style.display = 'block';
|
||||
document.getElementById('success-detail').textContent = `${googleUser.name} — $${data.total.toFixed(2)} total. You're all set!`;
|
||||
document.querySelector('.sticky-footer').style.display = 'none';
|
||||
} else {
|
||||
alert(data.error || 'Something went wrong.');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Submit Order';
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to submit. Check your connection and try again.');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Submit Order';
|
||||
}
|
||||
}
|
||||
"""
|
||||
else:
|
||||
submit_order_js = """
|
||||
async function submitOrder() {
|
||||
const name = document.getElementById('emp-name').value.trim();
|
||||
const email = document.getElementById('emp-email').value.trim();
|
||||
if (!name) { alert('Please enter your name.'); return; }
|
||||
if (!email) { alert('Please enter your email.'); return; }
|
||||
|
||||
const items = Object.entries(quantities).map(([i, qty]) => ({
|
||||
name: MEALS[i].name,
|
||||
retail_price: MEALS[i].price,
|
||||
quantity: qty,
|
||||
}));
|
||||
|
||||
const btn = document.getElementById('submit-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Submitting...';
|
||||
|
||||
try {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (API_KEY) headers['x-api-key'] = API_KEY;
|
||||
const res = await fetch(SUBMIT_URL, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ employee_name: name, employee_email: email, items }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
for (const el of document.getElementById('app').children) { if (el.id !== 'success') el.style.display = 'none'; }
|
||||
document.getElementById('success').style.display = 'block';
|
||||
document.getElementById('success-detail').textContent = `${name} — $${data.total.toFixed(2)} total. You're all set!`;
|
||||
document.querySelector('.sticky-footer').style.display = 'none';
|
||||
} else {
|
||||
alert(data.error || 'Something went wrong.');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Submit Order';
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to submit. Check your connection and try again.');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Submit Order';
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
|
@ -93,23 +266,54 @@ header p {{ font-size: 0.85rem; opacity: 0.8; }}
|
|||
.search-bar {{ width: 100%; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 8px; font-size: 1rem; margin-bottom: 16px; }}
|
||||
body {{ padding-bottom: 80px; }}
|
||||
.closed-banner {{ background: #fee2e2; color: #991b1b; padding: 16px; border-radius: 8px; margin-bottom: 20px; text-align: center; font-weight: 600; font-size: 1rem; }}
|
||||
@media (min-width: 768px) {{
|
||||
.container {{ max-width: 1100px; }}
|
||||
#meals-list {{ display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }}
|
||||
#meals-list .meal-card {{ margin-bottom: 0; }}
|
||||
}}
|
||||
@media (min-width: 1200px) {{
|
||||
.container {{ max-width: 1400px; }}
|
||||
#meals-list {{ grid-template-columns: repeat(3, 1fr); }}
|
||||
}}
|
||||
.discount-banner {{ background: #dcfce7; color: #15803d; padding: 10px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 0.85rem; text-align: center; font-weight: 500; }}
|
||||
.price-retail {{ text-decoration: line-through; color: #9ca3af; font-size: 0.75rem; margin-right: 4px; }}
|
||||
.price-employee {{ color: #15803d; font-weight: 600; }}
|
||||
.auth-overlay {{ position: fixed; inset: 0; background: #1a1a2e; display: flex; align-items: center; justify-content: center; z-index: 1000; }}
|
||||
.auth-card {{ background: #fff; border-radius: 16px; padding: 48px 40px; text-align: center; max-width: 400px; width: 90%; box-shadow: 0 8px 32px rgba(0,0,0,0.3); }}
|
||||
.auth-card h1 {{ font-size: 1.5rem; margin-bottom: 4px; color: #1d1d1f; }}
|
||||
.auth-card p {{ font-size: 0.9rem; color: #6b7280; margin-bottom: 32px; }}
|
||||
.auth-card .logo {{ font-size: 2rem; margin-bottom: 16px; }}
|
||||
.closed-overlay {{ position: fixed; inset: 0; background: #1a1a2e; display: none; align-items: center; justify-content: center; z-index: 2000; }}
|
||||
.closed-card {{ background: #fff; border-radius: 16px; padding: 48px 40px; text-align: center; max-width: 420px; width: 90%; box-shadow: 0 8px 32px rgba(0,0,0,0.3); }}
|
||||
.closed-card h1 {{ font-size: 1.5rem; margin-bottom: 8px; color: #1d1d1f; }}
|
||||
.closed-card p {{ font-size: 0.9rem; color: #6b7280; margin-bottom: 24px; }}
|
||||
.closed-card .logo {{ font-size: 2.5rem; margin-bottom: 16px; }}
|
||||
.countdown {{ font-size: 2rem; font-weight: 700; color: #1a1a2e; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }}
|
||||
.countdown-label {{ font-size: 0.75rem; color: #9ca3af; margin-top: 4px; }}
|
||||
</style>
|
||||
{'<script src="https://accounts.google.com/gsi/client" async defer></script>' if use_google_auth else ''}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container" id="app">
|
||||
<div class="closed-overlay" id="closed-overlay">
|
||||
<div class="closed-card">
|
||||
<div class="logo">🍽</div>
|
||||
<h1>Orders Are Closed</h1>
|
||||
<p>Orders open again Monday at 8:00 AM ET</p>
|
||||
<div class="countdown" id="countdown"></div>
|
||||
<div class="countdown-label">until orders open</div>
|
||||
</div>
|
||||
</div>
|
||||
{'<div class="auth-overlay" id="auth-overlay"><div class="auth-card"><div class="logo">🍽</div><h1>Sea Haven Meal Order</h1><p>Sign in with your company Google account to place your order.</p><div id="g-signin-btn" style="display:flex;justify-content:center;"></div></div></div>' if use_google_auth else ''}
|
||||
<div class="container" id="app" {'style="display:none;"' if use_google_auth else ''}>
|
||||
<header>
|
||||
<h1>Sea Haven Meal Order</h1>
|
||||
<p>Week of {week} · Menu scraped {scraped_at[:10]}</p>
|
||||
</header>
|
||||
|
||||
<div class="deadline">Order deadline: {deadline}</div>
|
||||
{'<div class="discount-banner">Prices reflect employee cost after ' + (f"{int(bulk_discount)}% bulk discount" if bulk_discount > 0 else "") + (" + " if bulk_discount > 0 and company_subsidy > 0 else "") + (f"{int(company_subsidy)}% company subsidy" if company_subsidy > 0 else "") + "</div>" if has_discount else ""}
|
||||
|
||||
<div class="employee-info">
|
||||
<label for="emp-name">Your Name</label>
|
||||
<select id="emp-name"><option value="">Loading...</option></select>
|
||||
<input type="hidden" id="emp-email">
|
||||
</div>
|
||||
{auth_section_html}
|
||||
|
||||
<input type="text" class="search-bar" id="search" placeholder="Search meals...">
|
||||
|
||||
|
|
@ -117,7 +321,7 @@ body {{ padding-bottom: 80px; }}
|
|||
|
||||
<div id="meals-list"></div>
|
||||
|
||||
<div class="sticky-footer">
|
||||
<div class="sticky-footer" {'style="display:none;" id="sticky-footer"' if use_google_auth else ''}>
|
||||
<div class="inner">
|
||||
<div>
|
||||
<span class="total" id="total-display">$0.00</span>
|
||||
|
|
@ -141,12 +345,21 @@ const STATUS_URL = '{status_url}';
|
|||
const ROSTER_URL = '{roster_url}';
|
||||
const API_KEY = {api_key_json};
|
||||
const WEEK = '{week}';
|
||||
const BULK_DISCOUNT = {bulk_discount};
|
||||
const COMPANY_SUBSIDY = {company_subsidy};
|
||||
const quantities = {{}};
|
||||
let formClosed = false;
|
||||
{'const GOOGLE_CLIENT_ID = ' + google_client_id_json + ';\nlet googleCredential = null;\nlet googleUser = null;' if use_google_auth else ''}
|
||||
|
||||
function employeePrice(retail) {{
|
||||
if (!retail) return 0;
|
||||
const bulk = retail * (1 - BULK_DISCOUNT / 100);
|
||||
return Math.round(bulk * (1 - COMPANY_SUBSIDY / 100) * 100) / 100;
|
||||
}}
|
||||
{google_auth_js}
|
||||
function init() {{
|
||||
checkFormStatus();
|
||||
loadRoster();
|
||||
{'waitForGoogleAuth();' if use_google_auth else 'loadRoster();'}
|
||||
// Build filter buttons
|
||||
const tags = new Set();
|
||||
MEALS.forEach(m => (m.dietary_tags || []).forEach(t => tags.add(t)));
|
||||
|
|
@ -189,14 +402,18 @@ function renderMeals() {{
|
|||
(meal.dietary_tags || []).forEach(t => {{ tagsHtml += `<span>${{t}}</span>`; }});
|
||||
|
||||
const descHtml = meal.description ? `<div class="meal-desc">${{meal.description}}</div>` : '';
|
||||
const price = meal.price ? `$${{meal.price.toFixed(2)}}` : '—';
|
||||
const hasDiscount = (BULK_DISCOUNT > 0 || COMPANY_SUBSIDY > 0) && meal.price;
|
||||
const empPrice = employeePrice(meal.price);
|
||||
const priceHtml = hasDiscount
|
||||
? `<span class="price-retail">$${{meal.price.toFixed(2)}}</span><span class="price-employee">$${{empPrice.toFixed(2)}}</span>`
|
||||
: (meal.price ? `$${{meal.price.toFixed(2)}}` : '—');
|
||||
|
||||
card.innerHTML = `
|
||||
${{meal.image_url ? `<img class="meal-img" src="${{meal.image_url}}" alt="${{meal.name}}" loading="lazy">` : ''}}
|
||||
<div class="meal-info">
|
||||
<div class="meal-name">${{meal.name}}</div>
|
||||
${{descHtml}}
|
||||
<div class="meal-meta">${{price}} · ${{meal.calories || '?'}} cal · ${{meal.protein || '?'}} protein</div>
|
||||
<div class="meal-meta">${{priceHtml}} · ${{meal.calories || '?'}} cal · ${{meal.protein || '?'}} protein</div>
|
||||
<div class="meal-tags">${{tagsHtml}}</div>
|
||||
</div>
|
||||
<div class="qty-control">
|
||||
|
|
@ -221,56 +438,15 @@ function changeQty(index, delta) {{
|
|||
function updateTotal() {{
|
||||
let total = 0, count = 0;
|
||||
Object.entries(quantities).forEach(([i, qty]) => {{
|
||||
total += (MEALS[i].price || 0) * qty;
|
||||
total += employeePrice(MEALS[i].price || 0) * qty;
|
||||
count += qty;
|
||||
}});
|
||||
document.getElementById('total-display').textContent = `$${{total.toFixed(2)}}`;
|
||||
document.getElementById('count-display').textContent = `${{count}} meal${{count !== 1 ? 's' : ''}}`;
|
||||
document.getElementById('submit-btn').disabled = count === 0;
|
||||
document.getElementById('submit-btn').disabled = count === 0{' || !googleCredential' if use_google_auth else ''};
|
||||
}}
|
||||
|
||||
async function submitOrder() {{
|
||||
const name = document.getElementById('emp-name').value.trim();
|
||||
const email = document.getElementById('emp-email').value.trim();
|
||||
if (!name) {{ alert('Please enter your name.'); return; }}
|
||||
if (!email) {{ alert('Please enter your email.'); return; }}
|
||||
|
||||
const items = Object.entries(quantities).map(([i, qty]) => ({{
|
||||
name: MEALS[i].name,
|
||||
price: MEALS[i].price,
|
||||
quantity: qty,
|
||||
subtotal: (MEALS[i].price || 0) * qty,
|
||||
}}));
|
||||
|
||||
const btn = document.getElementById('submit-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Submitting...';
|
||||
|
||||
try {{
|
||||
const headers = {{ 'Content-Type': 'application/json' }};
|
||||
if (API_KEY) headers['x-api-key'] = API_KEY;
|
||||
const res = await fetch(SUBMIT_URL, {{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({{ employee_name: name, employee_email: email, items }}),
|
||||
}});
|
||||
const data = await res.json();
|
||||
if (res.ok) {{
|
||||
document.getElementById('app').querySelectorAll(':not(#success)').forEach(el => el.style.display = 'none');
|
||||
document.getElementById('success').style.display = 'block';
|
||||
document.getElementById('success-detail').textContent = `${{name}} — $${{data.total.toFixed(2)}} total. You're all set!`;
|
||||
document.querySelector('.sticky-footer').style.display = 'none';
|
||||
}} else {{
|
||||
alert(data.error || 'Something went wrong.');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Submit Order';
|
||||
}}
|
||||
}} catch (e) {{
|
||||
alert('Failed to submit. Check your connection and try again.');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Submit Order';
|
||||
}}
|
||||
}}
|
||||
{submit_order_js}
|
||||
|
||||
async function loadRoster() {{
|
||||
try {{
|
||||
|
|
@ -309,18 +485,42 @@ async function checkFormStatus() {{
|
|||
const data = await res.json();
|
||||
if (data.status === 'closed') {{
|
||||
formClosed = true;
|
||||
const banner = document.createElement('div');
|
||||
banner.className = 'closed-banner';
|
||||
banner.textContent = 'Orders are closed for this week.';
|
||||
const deadline = document.querySelector('.deadline');
|
||||
if (deadline) deadline.replaceWith(banner);
|
||||
document.getElementById('submit-btn').disabled = true;
|
||||
document.getElementById('submit-btn').textContent = 'Closed';
|
||||
document.querySelectorAll('.qty-control button').forEach(b => b.disabled = true);
|
||||
document.getElementById('closed-overlay').style.display = 'flex';
|
||||
startCountdown();
|
||||
}}
|
||||
}} catch (e) {{}}
|
||||
}}
|
||||
|
||||
function startCountdown() {{
|
||||
updateCountdown();
|
||||
setInterval(updateCountdown, 1000);
|
||||
}}
|
||||
|
||||
function updateCountdown() {{
|
||||
const now = new Date();
|
||||
const etStr = now.toLocaleString('en-US', {{ timeZone: 'America/New_York', hour12: false }});
|
||||
const etDate = new Date(etStr);
|
||||
const day = etDate.getDay();
|
||||
const hour = etDate.getHours();
|
||||
|
||||
let daysUntil = (1 - day + 7) % 7;
|
||||
if (daysUntil === 0 && hour >= 8) daysUntil = 7;
|
||||
|
||||
const target = new Date(etDate);
|
||||
target.setDate(target.getDate() + daysUntil);
|
||||
target.setHours(8, 0, 0, 0);
|
||||
|
||||
let diff = Math.max(0, Math.floor((target - etDate) / 1000));
|
||||
const d = Math.floor(diff / 86400); diff %= 86400;
|
||||
const h = Math.floor(diff / 3600); diff %= 3600;
|
||||
const m = Math.floor(diff / 60);
|
||||
const s = diff % 60;
|
||||
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
document.getElementById('countdown').textContent =
|
||||
`${{d}}d ${{pad(h)}}h ${{pad(m)}}m ${{pad(s)}}s`;
|
||||
}}
|
||||
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
|
|
@ -335,11 +535,56 @@ def main():
|
|||
parser.add_argument(
|
||||
"--api-key", default="", help="API key for order submission (cloud mode)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bulk-discount",
|
||||
type=float,
|
||||
default=None,
|
||||
help="Bulk discount percentage (e.g., 10 for 10%% off)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--company-subsidy",
|
||||
type=float,
|
||||
default=None,
|
||||
help="Company subsidy percentage (e.g., 50 for 50%% off after bulk discount)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--google-client-id",
|
||||
default=None,
|
||||
help="Google OAuth Client ID for Sign-In authentication",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
config = load_config()
|
||||
menu = latest_menu()
|
||||
html = generate_form(menu, config, api_url=args.api_url, api_key=args.api_key)
|
||||
|
||||
bulk_discount = (
|
||||
args.bulk_discount
|
||||
if args.bulk_discount is not None
|
||||
else config.get("bulk_discount_percent", 0)
|
||||
)
|
||||
company_subsidy = (
|
||||
args.company_subsidy
|
||||
if args.company_subsidy is not None
|
||||
else config.get("company_subsidy_percent", 0)
|
||||
)
|
||||
google_client_id = args.google_client_id or config.get("google_client_id", "")
|
||||
if not google_client_id:
|
||||
try:
|
||||
ssm = boto3.client("ssm")
|
||||
resp = ssm.get_parameter(Name="/meal-order-manager/google-client-id")
|
||||
google_client_id = resp["Parameter"]["Value"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
html = generate_form(
|
||||
menu,
|
||||
config,
|
||||
api_url=args.api_url,
|
||||
api_key=args.api_key,
|
||||
bulk_discount=bulk_discount,
|
||||
company_subsidy=company_subsidy,
|
||||
google_client_id=google_client_id,
|
||||
)
|
||||
|
||||
week = datetime.now().strftime("%Y-W%U")
|
||||
output_file = OUTPUT_DIR / f"order-form-{week}.html"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue