From f437ca8f0f57432d13912f2167a46d27448c733c Mon Sep 17 00:00:00 2001
From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com>
Date: Wed, 13 May 2026 11:36:14 -0400
Subject: [PATCH] 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.
---
.github/workflows/weekly-menu.yml | 37 ++-
src/server/generate_form.py | 377 ++++++++++++++++++++++++------
2 files changed, 347 insertions(+), 67 deletions(-)
diff --git a/.github/workflows/weekly-menu.yml b/.github/workflows/weekly-menu.yml
index 3c03083..214fbf0 100644
--- a/.github/workflows/weekly-menu.yml
+++ b/.github/workflows/weekly-menu.yml
@@ -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
diff --git a/src/server/generate_form.py b/src/server/generate_form.py
index e601e08..4d28a65 100644
--- a/src/server/generate_form.py
+++ b/src/server/generate_form.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 = """
+
+
+
![]()
+
+
+
+
+
"""
+ else:
+ auth_section_html = """
+
+
+
+
"""
+
+ 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"""
@@ -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; }}
+{'' if use_google_auth else ''}
-
-
+
+
+
🍽
+
Orders Are Closed
+
Orders open again Monday at 8:00 AM ET
+
+
until orders open
+
+
+{'
🍽
Sea Haven Meal Order
Sign in with your company Google account to place your order.
' if use_google_auth else ''}
+
Order deadline: {deadline}
+ {'
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 "") + "
" if has_discount else ""}
-
-
-
-
-
+{auth_section_html}
@@ -117,7 +321,7 @@ body {{ padding-bottom: 80px; }}
-