diff --git a/README.md b/README.md index b6b8f68..7909b05 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ who haven't ordered | Monday 6:55am ET | Sync employee roster from Slack channel membership | EventBridge → Lambda → DynamoDB | | Monday 7am ET | Email previous week's payroll deductions to `payroll@` | EventBridge → Lambda → SES | | Monday 8am ET | Scrape menu, generate form, upload to S3, post link to Slack | GitHub Actions cron | -| Mon–Thu | Employees visit `orders.seahavenind.com` and submit orders | S3 static form → API Gateway → Lambda → DynamoDB | +| Mon–Thu | Employees visit `orders.seahaven.com` and submit orders | S3 static form → API Gateway → Lambda → DynamoDB | | Thursday 10am ET | DM employees who haven't ordered yet | EventBridge → Lambda → Slack DM | | Thursday 6pm ET | Close form, aggregate orders, post Redefine order summary to Slack | EventBridge → Lambda chain | @@ -41,7 +41,7 @@ who haven't ordered Stack name: `meal-order-manager` (us-east-1) - **S3** — `meal-order-manager-form-*` (static form hosting), `meal-order-manager-reports-*` (CSV reports) -- **CloudFront** — HTTPS distribution with custom domain `orders.seahavenind.com` +- **CloudFront** — HTTPS distribution with custom domain `orders.seahaven.com` - **DynamoDB** — `meal-order-manager-orders` (orders, menu, roster, config) - **API Gateway** — HttpApi for order submission - **Lambda** — 6 functions: submit-order, close-form, aggregate-orders, slack-notifier, sync-roster, email-report @@ -75,7 +75,7 @@ sam deploy 2. Create the form API key secret: `aws secretsmanager create-secret --name meal-order-manager/form-api-key --secret-string "$(openssl rand -hex 32)"` 3. Update the Slack channel SSM parameter: `aws ssm put-parameter --name /meal-order-manager/slack-channel-id --value "C0XXXXXXX" --overwrite` 4. Verify SES sender identity for `adam@seahavenind.com` -5. Set up DNS: CNAME `orders.seahavenind.com` → CloudFront distribution domain +5. Set up DNS: CNAME `orders.seahaven.com` → CloudFront distribution domain 6. Roster syncs automatically from Slack channel members (runs Monday 6:55am ET), or seed manually: `python3 scripts/seed_roster.py` ## Local Workflow (no AWS) diff --git a/functions/submit_order/handler.py b/functions/submit_order/handler.py index 9ff9939..2841c6b 100644 --- a/functions/submit_order/handler.py +++ b/functions/submit_order/handler.py @@ -3,7 +3,7 @@ import os from datetime import datetime from zoneinfo import ZoneInfo -from shared.db import current_week, get_form_status, put_order +from shared.db import current_week, get_form_status, get_roster, put_order from shared.secrets import get_secret EASTERN = ZoneInfo("America/New_York") @@ -24,6 +24,9 @@ def lambda_handler(event, context): if "/form-status/" in path: return handle_form_status(event) + if "/roster" in path: + return handle_roster() + if method == "POST": return handle_submit(event) @@ -36,6 +39,12 @@ def handle_form_status(event): return response(200, {"week": week, "status": status}) +def handle_roster(): + roster = get_roster() + employees = [{"name": e["name"], "email": e["email"]} for e in roster] + return response(200, {"employees": employees}) + + def handle_submit(event): api_key = event.get("headers", {}).get("x-api-key", "") if api_key != _get_api_key(): diff --git a/samconfig.toml.example b/samconfig.toml.example index 788af9b..328f48c 100644 --- a/samconfig.toml.example +++ b/samconfig.toml.example @@ -7,4 +7,4 @@ s3_prefix = "meal-order-manager" region = "us-east-1" confirm_changeset = true capabilities = "CAPABILITY_IAM" -parameter_overrides = "CustomDomain=orders.seahavenind.com CertificateArn=arn:aws:acm:us-east-1:328440206208:certificate/CHANGE-ME PayrollEmail=payroll@seahavenind.com SenderEmail=adam@seahavenind.com" +parameter_overrides = "CustomDomain=orders.seahaven.com CertificateArn=arn:aws:acm:us-east-1:328440206208:certificate/CHANGE-ME PayrollEmail=payroll@seahavenind.com SenderEmail=adam@seahavenind.com" diff --git a/src/server/generate_form.py b/src/server/generate_form.py index d67e642..e601e08 100644 --- a/src/server/generate_form.py +++ b/src/server/generate_form.py @@ -39,11 +39,12 @@ def generate_form( ) -> str: meals_json = json.dumps(menu["meals"]) roster_json = json.dumps(config.get("roster", [])) - deadline = config.get("order_deadline", "Thursday 6:00 PM") + deadline = config.get("order_deadline", "Thursday 11:59 PM") 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 "" + roster_url = f"{api_url}/api/roster" if api_url else "/api/roster" api_key_json = json.dumps(api_key) return f""" @@ -106,9 +107,8 @@ body {{ padding-bottom: 80px; }}
- - - + +
@@ -138,6 +138,7 @@ const MEALS = {meals_json}; const ROSTER = {roster_json}; const SUBMIT_URL = '{submit_url}'; const STATUS_URL = '{status_url}'; +const ROSTER_URL = '{roster_url}'; const API_KEY = {api_key_json}; const WEEK = '{week}'; const quantities = {{}}; @@ -145,6 +146,7 @@ let formClosed = false; function init() {{ checkFormStatus(); + loadRoster(); // Build filter buttons const tags = new Set(); MEALS.forEach(m => (m.dietary_tags || []).forEach(t => tags.add(t))); @@ -270,6 +272,36 @@ async function submitOrder() {{ }} }} +async function loadRoster() {{ + try {{ + const res = await fetch(ROSTER_URL); + const data = await res.json(); + const select = document.getElementById('emp-name'); + select.innerHTML = ''; + (data.employees || ROSTER).forEach(emp => {{ + const opt = document.createElement('option'); + opt.value = emp.name; + opt.dataset.email = emp.email; + opt.textContent = emp.name; + select.appendChild(opt); + }}); + select.addEventListener('change', () => {{ + const selected = select.options[select.selectedIndex]; + document.getElementById('emp-email').value = selected?.dataset?.email || ''; + }}); + }} catch (e) {{ + const select = document.getElementById('emp-name'); + select.innerHTML = ''; + ROSTER.forEach(emp => {{ + const opt = document.createElement('option'); + opt.value = emp.name; + opt.dataset.email = emp.email; + opt.textContent = emp.name; + select.appendChild(opt); + }}); + }} +}} + async function checkFormStatus() {{ if (!STATUS_URL) return; try {{ diff --git a/template.yaml b/template.yaml index 26d20ac..b82969f 100644 --- a/template.yaml +++ b/template.yaml @@ -7,7 +7,7 @@ Description: > Parameters: CustomDomain: Type: String - Default: orders.seahavenind.com + Default: orders.seahaven.com Description: Custom domain for the order form (requires ACM cert) CertificateArn: Type: String @@ -250,6 +250,12 @@ Resources: ApiId: !Ref OrderApi Path: /api/form-status/{week} Method: GET + Roster: + Type: HttpApi + Properties: + ApiId: !Ref OrderApi + Path: /api/roster + Method: GET CloseFormFunction: Type: AWS::Serverless::Function