diff --git a/functions/slack_notifier/handler.py b/functions/slack_notifier/handler.py
index d9387dd..f4324dd 100644
--- a/functions/slack_notifier/handler.py
+++ b/functions/slack_notifier/handler.py
@@ -4,7 +4,7 @@ from datetime import datetime
from decimal import Decimal
from zoneinfo import ZoneInfo
-from shared.db import current_week, get_orders, get_roster, get_summary
+from shared.db import current_week, get_orders, get_roster, get_settings, get_summary
from shared.slack import post_channel_message, send_dm
@@ -121,8 +121,23 @@ def handle_orders_aggregated(event):
},
]
- post_channel_message(text, blocks)
- return {"status": "notified", "event": "orders_aggregated", "week": week}
+ settings = get_settings()
+ admin_emails = [e.lower() for e in settings.get("admin_emails", [])]
+ roster = get_roster()
+ dm_count = 0
+ for email in admin_emails:
+ employee = next((e for e in roster if e["email"].lower() == email), None)
+ if not employee or not employee.get("slack_user_id"):
+ continue
+ send_dm(employee["slack_user_id"], text, blocks)
+ dm_count += 1
+
+ return {
+ "status": "notified",
+ "event": "orders_aggregated",
+ "week": week,
+ "admin_dm_count": dm_count,
+ }
def handle_order_confirmed(event):
diff --git a/functions/submit_order/handler.py b/functions/submit_order/handler.py
index a1178cc..c90fcae 100644
--- a/functions/submit_order/handler.py
+++ b/functions/submit_order/handler.py
@@ -429,7 +429,11 @@ def handle_submit(event):
week = current_week()
status = get_form_status(week)
- if status == "closed":
+ is_admin_user = False
+ if _google_auth_configured():
+ admin_emails = {e.lower() for e in get_settings().get("admin_emails", [])}
+ is_admin_user = email.lower() in admin_emails
+ if status == "closed" and not is_admin_user:
return response(410, {"error": "Orders are closed for this week"})
if status == "not_found":
return response(404, {"error": "No menu available for this week"})
diff --git a/src/server/generate_form.py b/src/server/generate_form.py
index baafdad..fed517f 100644
--- a/src/server/generate_form.py
+++ b/src/server/generate_form.py
@@ -151,7 +151,7 @@ function signOut() {
if use_google_auth:
submit_order_js = """
async function submitOrder() {
- if (formClosed) { alert('Orders are closed.'); return; }
+ if (formClosed && !isAdmin) { alert('Orders are closed.'); return; }
if (!googleCredential || !googleUser) { alert('Please sign in with Google first.'); return; }
const items = Object.entries(quantities).map(([i, qty]) => ({
@@ -194,7 +194,7 @@ async function submitOrder() {
else:
submit_order_js = """
async function submitOrder() {
- if (formClosed) { alert('Orders are closed.'); return; }
+ if (formClosed && !isAdmin) { alert('Orders are closed.'); return; }
const name = document.getElementById('emp-name').value.trim();
const email = document.getElementById('emp-email').value.trim();
if (!name) { alert('Please enter your name.'); return; }
@@ -555,9 +555,9 @@ function renderMeals() {{
${{tagsHtml}}
-
+
-
+
`;
list.appendChild(card);
@@ -622,6 +622,23 @@ function updateTotal() {{
}
'''
}
+function adminBypassClosed() {{
+ document.getElementById('closed-overlay').style.display = 'none';
+ document.querySelectorAll('.qty-control button').forEach(b => b.disabled = false);
+ var submitBtn = document.getElementById('submit-btn');
+ if (submitBtn) submitBtn.disabled = Object.keys(quantities).length === 0;
+ var existing = document.getElementById('admin-closed-banner');
+ if (!existing) {{
+ var banner = document.createElement('div');
+ banner.id = 'admin-closed-banner';
+ banner.className = 'closed-banner';
+ banner.textContent = 'Form is closed — admin override active';
+ var deadline = document.querySelector('.deadline');
+ if (deadline) deadline.parentNode.insertBefore(banner, deadline.nextSibling);
+ }}
+ renderMeals();
+}}
+
function checkAdmin() {{
if (!ADMIN_URL) return;
const headers = {{}};
@@ -636,6 +653,7 @@ function checkAdmin() {{
adminWeeks = data.weeks;
var ab = document.getElementById('admin-btn');
if (ab) ab.style.display = 'inline-block';
+ if (formClosed) adminBypassClosed();
}}
}})
.catch(() => {{}});
@@ -844,11 +862,15 @@ async function checkFormStatus() {{
const data = await res.json();
if (data.status === 'closed') {{
formClosed = true;
- document.getElementById('closed-overlay').style.display = 'flex';
- document.querySelectorAll('.qty-control button').forEach(b => b.disabled = true);
- const submitBtn = document.getElementById('submit-btn');
- if (submitBtn) submitBtn.disabled = true;
- startCountdown(data);
+ if (isAdmin) {{
+ adminBypassClosed();
+ }} else {{
+ document.getElementById('closed-overlay').style.display = 'flex';
+ document.querySelectorAll('.qty-control button').forEach(b => b.disabled = true);
+ const submitBtn = document.getElementById('submit-btn');
+ if (submitBtn) submitBtn.disabled = true;
+ startCountdown(data);
+ }}
}}
}} catch (e) {{}}
}}
diff --git a/tests/test_submit_order.py b/tests/test_submit_order.py
index 535fa5d..9caeaf1 100644
--- a/tests/test_submit_order.py
+++ b/tests/test_submit_order.py
@@ -1297,6 +1297,120 @@ def test_submit_form_closed(
mock_put.assert_not_called()
+@patch("submit_order_handler._lambda")
+@patch("submit_order_handler.put_order")
+@patch("submit_order_handler.get_form_status", return_value="closed")
+@patch("submit_order_handler.current_week", return_value="2026-W20")
+@patch(
+ "submit_order_handler.get_settings",
+ return_value={
+ "bulk_discount_percent": 0,
+ "company_subsidy_percent": 0,
+ "admin_emails": ["adam@seahavenind.com"],
+ },
+)
+@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
+@patch(
+ "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
+)
+@patch("submit_order_handler.urllib.request.urlopen")
+@patch("submit_order_handler._google_auth_configured", return_value=True)
+@patch("submit_order_handler.get_menu")
+def test_admin_can_submit_when_form_closed(
+ mock_get_menu,
+ mock_gac,
+ mock_urlopen,
+ mock_gcid,
+ mock_secret,
+ mock_settings,
+ mock_week,
+ mock_status,
+ mock_put,
+ mock_lam,
+):
+ """Admin user can submit even when form is closed."""
+ from submit_order_handler import lambda_handler
+
+ mock_resp = MagicMock()
+ mock_resp.read.return_value = json.dumps(
+ {
+ "aud": VALID_GOOGLE_CLIENT_ID,
+ "hd": "seahavenind.com",
+ "name": "Adam Moussa",
+ "email": "adam@seahavenind.com",
+ }
+ ).encode()
+ mock_resp.__enter__ = MagicMock(return_value=mock_resp)
+ mock_resp.__exit__ = MagicMock(return_value=False)
+ mock_urlopen.return_value = mock_resp
+
+ items = _make_items([(10.00, 1)])
+ mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
+ event = _submit_event(items, extra_body={"google_id_token": "admin-token"})
+ result = lambda_handler(event, None)
+ status, body = _parse_response(result)
+
+ assert status == 200, f"Admin should bypass closed form, got {status}: {body}"
+ mock_put.assert_called_once()
+
+
+@patch("submit_order_handler._lambda")
+@patch("submit_order_handler.put_order")
+@patch("submit_order_handler.get_form_status", return_value="closed")
+@patch("submit_order_handler.current_week", return_value="2026-W20")
+@patch(
+ "submit_order_handler.get_settings",
+ return_value={
+ "bulk_discount_percent": 0,
+ "company_subsidy_percent": 0,
+ "admin_emails": ["adam@seahavenind.com"],
+ },
+)
+@patch("submit_order_handler.get_secret", return_value=TEST_API_KEY)
+@patch(
+ "submit_order_handler._get_google_client_id", return_value=VALID_GOOGLE_CLIENT_ID
+)
+@patch("submit_order_handler.urllib.request.urlopen")
+@patch("submit_order_handler._google_auth_configured", return_value=True)
+@patch("submit_order_handler.get_menu")
+def test_non_admin_blocked_when_form_closed(
+ mock_get_menu,
+ mock_gac,
+ mock_urlopen,
+ mock_gcid,
+ mock_secret,
+ mock_settings,
+ mock_week,
+ mock_status,
+ mock_put,
+ mock_lam,
+):
+ """Non-admin user still blocked when form is closed."""
+ from submit_order_handler import lambda_handler
+
+ mock_resp = MagicMock()
+ mock_resp.read.return_value = json.dumps(
+ {
+ "aud": VALID_GOOGLE_CLIENT_ID,
+ "hd": "seahavenind.com",
+ "name": "Regular Employee",
+ "email": "employee@seahavenind.com",
+ }
+ ).encode()
+ mock_resp.__enter__ = MagicMock(return_value=mock_resp)
+ mock_resp.__exit__ = MagicMock(return_value=False)
+ mock_urlopen.return_value = mock_resp
+
+ items = _make_items([(10.00, 1)])
+ mock_get_menu.return_value = _menu_doc_from_retail_pairs([(10.00, 1)])
+ event = _submit_event(items, extra_body={"google_id_token": "user-token"})
+ result = lambda_handler(event, None)
+ status, body = _parse_response(result)
+
+ assert status == 410, f"Non-admin should be blocked, got {status}: {body}"
+ mock_put.assert_not_called()
+
+
@patch("submit_order_handler._lambda")
@patch("submit_order_handler.put_order")
@patch("submit_order_handler.get_form_status", return_value="not_found")