mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-10-02 09:33:11 +00:00
Switch to orders.seahaven.com, add roster dropdown, fix deadline (#9)
Some checks are pending
Deploy / deploy (push) Waiting to run
Some checks are pending
Deploy / deploy (push) Waiting to run
- Change custom domain from orders.seahavenind.com to orders.seahaven.com to match other subdomain conventions - Add GET /api/roster endpoint returning employee names/emails - Replace name/email text inputs with dropdown populated from roster API (falls back to embedded roster for local dev) - Fix order deadline text from Wednesday to Thursday 11:59 PM - Fix Slack API calls: use form-urlencoded for conversations and users methods that reject JSON body encoding
This commit is contained in:
parent
11ea599bbd
commit
a8adbdc116
5 changed files with 57 additions and 10 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"""<!DOCTYPE html>
|
||||
|
|
@ -106,9 +107,8 @@ body {{ padding-bottom: 80px; }}
|
|||
|
||||
<div class="employee-info">
|
||||
<label for="emp-name">Your Name</label>
|
||||
<input type="text" id="emp-name" placeholder="Full name" autocomplete="name">
|
||||
<label for="emp-email">Your Email</label>
|
||||
<input type="email" id="emp-email" placeholder="you@seahavenind.com" autocomplete="email">
|
||||
<select id="emp-name"><option value="">Loading...</option></select>
|
||||
<input type="hidden" id="emp-email">
|
||||
</div>
|
||||
|
||||
<input type="text" class="search-bar" id="search" placeholder="Search meals...">
|
||||
|
|
@ -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 = '<option value="">Select your name</option>';
|
||||
(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 = '<option value="">Select your name</option>';
|
||||
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 {{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue