mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 06:33:12 +00:00
Add admin form bypass and send order summary to admin DMs (#15)
Admins (by email in settings) can now view and submit orders even after the form closes. The orders-aggregated Slack summary is sent as a DM to each admin instead of posting to the channel.
This commit is contained in:
parent
5db95ce9d1
commit
1aa28b38fd
4 changed files with 168 additions and 13 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"})
|
||||
|
|
|
|||
|
|
@ -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() {{
|
|||
<div class="meal-tags">${{tagsHtml}}</div>
|
||||
</div>
|
||||
<div class="qty-control">
|
||||
<button onclick="changeQty(${{i}}, -1)" ${{formClosed ? 'disabled' : ''}}>−</button>
|
||||
<button onclick="changeQty(${{i}}, -1)" ${{formClosed && !isAdmin ? 'disabled' : ''}}>−</button>
|
||||
<input class="qty" type="text" value="${{qty}}" readonly>
|
||||
<button onclick="changeQty(${{i}}, 1)" ${{formClosed ? 'disabled' : ''}}>+</button>
|
||||
<button onclick="changeQty(${{i}}, 1)" ${{formClosed && !isAdmin ? 'disabled' : ''}}>+</button>
|
||||
</div>
|
||||
`;
|
||||
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) {{}}
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue