+
+ )
+}
diff --git a/app/admin/meal/page.tsx b/app/admin/meal/page.tsx
new file mode 100644
index 00000000..65ed461f
--- /dev/null
+++ b/app/admin/meal/page.tsx
@@ -0,0 +1,29 @@
+import React from "react";
+import Layout from "@components/layout/layout";
+import { Container } from "@components/layout/container";
+import Navigation from "@components/admin/navigation";
+import DiningPageClient from "./meal-client"
+// import { currentUser } from "@clerk/nextjs/server";
+
+const pages = [
+ { name: 'Admin', href: '/admin', current: true },
+ { name: 'Dining', href: '/admin/dining', current: true },
+]
+
+export default async function AdminUserPage() {
+
+ // const loggedInUser = await currentUser();
+
+ return (
+
+ {" "}
+
+
+
+
+
+
+
+
+);
+}
\ No newline at end of file
diff --git a/app/api/admin/meal/trigger/route.ts b/app/api/admin/meal/trigger/route.ts
new file mode 100644
index 00000000..e4dead3d
--- /dev/null
+++ b/app/api/admin/meal/trigger/route.ts
@@ -0,0 +1,29 @@
+import { createClerkClient } from '@clerk/backend';
+import { auth } from '@clerk/nextjs/server';
+import { NextRequest } from 'next/server';
+
+export async function POST(_req: NextRequest) {
+ const clerkClient = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY });
+ const {userId} = auth();
+
+ if(!userId){
+ return Response.json({error: "User is not signed in."}, { status: 401 });
+ }
+
+ const requestingUser = await clerkClient.users.getUser(userId);
+ if(!requestingUser.publicMetadata.admin){
+ return Response.json({error: "User is does not have permissions."}, { status: 401 });
+ }
+
+ const triggerUrl = process.env.LAMBDA_MEAL_TRIGGER_REMINDER
+ console.log(triggerUrl)
+ const triggerResponse = await fetch(triggerUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ })
+
+ return triggerResponse.ok ? Response.json({message: "Email triggered"}, {status: 200}) : Response.json({message: "Email Failed"}, {status: 500})
+
+}
\ No newline at end of file
diff --git a/components/admin/hub.tsx b/components/admin/hub.tsx
index 2198b847..991e305f 100644
--- a/components/admin/hub.tsx
+++ b/components/admin/hub.tsx
@@ -70,8 +70,8 @@ const actions = [
state: 'live',
},
{
- title: 'Dining Information',
- href: '#',
+ title: 'Meal Information',
+ href: '/admin/meal',
description: "Meal information and seating plans",
icon: CakeIcon,
iconForeground: 'text-green-800',
diff --git a/functions/meal_groups/lambda_function.py b/functions/meal_groups/lambda_function.py
new file mode 100644
index 00000000..2e8a22f7
--- /dev/null
+++ b/functions/meal_groups/lambda_function.py
@@ -0,0 +1,36 @@
+import json
+import os
+import logging
+
+import boto3
+from boto3.dynamodb.conditions import Key, Attr
+from shared import DecimalEncoder
+
+logger = logging.getLogger()
+logger.setLevel(logging.INFO)
+
+## ENV
+event_table_name = os.environ.get("EVENT_TABLE_NAME")
+logger.info(event_table_name)
+
+dynamodb_client = boto3.client('dynamodb')
+db = boto3.resource('dynamodb')
+table = db.Table(event_table_name)
+
+def err(msg:str, code=400, logmsg=None, **kwargs):
+ logmsg = logmsg if logmsg else msg
+ logger.error(logmsg)
+ for k in kwargs:
+ logger.error(k+":"+kwargs[k])
+ return {
+ 'statusCode': code,
+ 'body': json.dumps({'error': msg})
+ }
+
+def lambda_handler(event, context):
+ scan_response = table.scan(FilterExpression=Key('PK').begins_with('GROUP#'))
+
+ return {
+ 'statusCode': 200,
+ 'body': json.dumps(scan_response, cls=DecimalEncoder.DecimalEncoder)
+ }
\ No newline at end of file
diff --git a/functions/meal_reminder/lambda_function.py b/functions/meal_reminder/lambda_function.py
new file mode 100644
index 00000000..9f6f0bc4
--- /dev/null
+++ b/functions/meal_reminder/lambda_function.py
@@ -0,0 +1,72 @@
+import json
+import logging
+
+import boto3
+from boto3.dynamodb.conditions import Key, Attr
+from shared import DecimalEncoder
+import os
+import time
+
+## ENV
+attendees_table_name = os.environ.get("ATTENDEES_TABLE_NAME")
+send_email_lambda = os.environ.get("SEND_EMAIL_LAMBDA")
+event_table_name = os.environ.get("EVENT_TABLE_NAME")
+
+logger = logging.getLogger()
+logger.setLevel("INFO")
+
+# profile_name='danceengine-admin'
+# boto3.setup_default_session(profile_name=profile_name)
+# logging.basicConfig()
+
+db = boto3.resource('dynamodb')
+table = db.Table(attendees_table_name)
+event_table = db.Table(event_table_name)
+
+lambda_client = boto3.client('lambda')
+
+def err(msg:str, code=400, logmsg=None, **kwargs):
+ logmsg = logmsg if logmsg else msg
+ logger.error(logmsg)
+ for k in kwargs:
+ logger.error(k+":"+kwargs[k])
+ return {
+ 'statusCode': code,
+ 'body': json.dumps({'error': msg})
+ }
+
+def lambda_handler(event, context):
+ logger.info(f"Event {event}")
+ if event['requestContext']['http']['method'] != "POST": return err("Method not allowed, make POST request", code=405)
+
+ response = table.scan(FilterExpression=Key('active').eq(True))
+
+ filtered_items = [item for item in response['Items'] if (item['access'][2] == 1) and ('meal_preferences' not in item or item['meal_preferences'] == None or any(choice == -1 for choice in item['meal_preferences']['choices']))]
+ # logger.info(filtered_items)
+
+ #! If no matches should problably return a 404
+ for item in filtered_items:
+ # send the email with these details
+ logger.info("Invoking send_email lambda")
+ response = lambda_client.invoke(
+ FunctionName=send_email_lambda,
+ InvocationType='Event',
+ Payload=json.dumps({
+ 'email_type':"meal_reminder",
+ 'email':item['email'],
+ 'ticket_number':item['ticket_number'],
+ }, cls=DecimalEncoder.DecimalEncoder),
+ )
+ logger.info(response)
+
+ event_table.put_item(
+ Item={
+ 'PK': "EMAIL#MEALREMINDER".format(),
+ 'SK': "DETAIL#{}".format(time.time()),
+ 'timestamp': "{}".format(time.time())
+ }
+ )
+
+ return {'statusCode': 200 }
+
+# lambda_handler({'requestContext':{'http':{'method':"GET"}}}, None)
\ No newline at end of file
diff --git a/functions/meal_seating/lambda_function.py b/functions/meal_seating/lambda_function.py
new file mode 100644
index 00000000..e69de29b
diff --git a/functions/meal_summary/lambda_function.py b/functions/meal_summary/lambda_function.py
new file mode 100644
index 00000000..40d13089
--- /dev/null
+++ b/functions/meal_summary/lambda_function.py
@@ -0,0 +1,110 @@
+import json
+import logging
+
+import boto3
+from boto3.dynamodb.conditions import Key, Attr
+from shared import DecimalEncoder
+import os
+
+## ENV
+attendees_table_name = os.environ.get("ATTENDEES_TABLE_NAME")
+event_table_name = os.environ.get("EVENT_TABLE_NAME")
+
+logger = logging.getLogger()
+logger.setLevel("INFO")
+
+# profile_name='danceengine-admin'
+# boto3.setup_default_session(profile_name=profile_name)
+# logging.basicConfig()
+
+db = boto3.resource('dynamodb')
+table = db.Table(attendees_table_name)
+event_table = db.Table(event_table_name)
+
+lambda_client = boto3.client('lambda')
+
+def err(msg:str, code=400, logmsg=None, **kwargs):
+ logmsg = logmsg if logmsg else msg
+ logger.error(logmsg)
+ for k in kwargs:
+ logger.error(k+":"+kwargs[k])
+ return {
+ 'statusCode': code,
+ 'body': json.dumps({'error': msg})
+ }
+
+def get_last_email():
+ response = table.scan(FilterExpression=Key('PK').eq("EMAIL#MEALREMINDER"))
+ sorted_list = sorted(response['Items'], key=lambda item: item['timestamp']) if len(response['Items'] > 0) else None
+
+ return sorted_list
+
+def lambda_handler(event, context):
+ logger.info(f"Event {event}")
+ if event['requestContext']['http']['method'] != "GET": return err("Method not allowed, make GET request", code=405)
+
+ response = table.scan(FilterExpression=Key('active').eq(True))
+
+ course_mappings = [
+ {0: "Vegetable Terrine", 1: "Chicken Liver Pate"},
+ {0: "Roasted Onion", 1: "Fish and Prawn Risotto", 2: "Chicken Supreme"},
+ {0: "Fruit Platter", 1: "Bread and Butter Pudding"}
+ ]
+ num_courses = 3
+ course_frequencies = [{} for _ in range(num_courses)]
+
+ not_selected_count = 0 # All choices are -1 (not selected)
+ incomplete_count = 0 # Some choices are -1 (incomplete selection)
+ not_wanted_count = 0 # Any choice is -99 (not wanted)
+ selected_count = 0 # All choices are >= 0 (options selected)
+
+ filtered_items = [item for item in response['Items'] if (item['access'][2] == 1)]
+
+ #! If no matches should problably return a 404
+ for item in filtered_items:
+ if ('meal_preferences' not in item):
+ not_selected_count += 1
+ continue
+ elif (item['meal_preferences'] is None):
+ not_selected_count += 1
+ continue
+
+ meal_prefs = item['meal_preferences']
+ if meal_prefs and 'choices' in meal_prefs:
+ choices = meal_prefs['choices']
+
+ if all(choice == -1 for choice in choices):
+ not_selected_count += 1
+ elif any(choice == -1 for choice in choices):
+ incomplete_count += 1
+ elif any(choice == -99 for choice in choices):
+ not_wanted_count += 1
+ elif all(choice >= 0 for choice in choices):
+ selected_count += 1
+
+ for i, choice in enumerate(choices):
+ if choice >= 0 and choice in course_mappings[i]:
+ dish_name = course_mappings[i][choice]
+ if dish_name in course_frequencies[i]:
+ course_frequencies[i][dish_name] += 1
+ else:
+ course_frequencies[i][dish_name] = 1
+
+ logger.info(f"Statistics: Not selected: {not_selected_count}, Incomplete: {incomplete_count}, Not wanted: {not_wanted_count}, Selected: {selected_count}")
+ logger.info(f"Course Frequencies: {course_frequencies}")
+
+ return {
+ 'statusCode': 200,
+ 'body': json.dumps({
+ 'statistics': {
+ 'not_selected_count': not_selected_count,
+ 'incomplete_count': incomplete_count,
+ 'not_wanted_count': not_wanted_count,
+ 'selected_count': selected_count,
+ 'course_frequencies': course_frequencies,
+ 'reminders_sent': get_last_email()
+ }}, cls=DecimalEncoder.DecimalEncoder)
+ }
+
+# from pprint import pprint
+# pprint(lambda_handler({'requestContext':{'http':{'method':"GET"}}}, None))
\ No newline at end of file
diff --git a/functions/send_email/lambda_function.py b/functions/send_email/lambda_function.py
index 993596d6..3d901c21 100644
--- a/functions/send_email/lambda_function.py
+++ b/functions/send_email/lambda_function.py
@@ -144,7 +144,7 @@ def lambda_handler(event, context):
body_tmpl = Template(body_file.read())
subdomain = "www" if os.environ.get("STAGE_NAME") == "prod" else os.environ.get("STAGE_NAME")
body = body_tmpl.substitute({
- 'deanline_date':deadline_date,
+ 'deadline_date':deadline_date,
'ticket_link':"http://{}.merseysidelatinfestival.co.uk/preferences?email={}&ticket_number={}".format(subdomain, event['email'], event['ticket_number']),
})
attachment = None
diff --git a/functions/send_email/meal_body.html b/functions/send_email/meal_body.html
index 3a685b81..6b015c46 100644
--- a/functions/send_email/meal_body.html
+++ b/functions/send_email/meal_body.html
@@ -29,12 +29,10 @@
Set your meal preferences!
It will be an unforgettable evening with great food and even better dance moves.
Take a quick moment to select your meal options now.
-
You can modify your options up until the $deadline_date, after this date you can no longer select options and it will be set to the default option.
If you have purchased multiple tickets with dinner included you will need to select the options for each ticket.
-
To manage your ticket click the link below and you will be able to select your meal options.
If you have a table preference you can join a group and we will try out best to sit you with the people in your group.
diff --git a/functions/serverless.yml b/functions/serverless.yml
index 9cbab847..2bcced89 100644
--- a/functions/serverless.yml
+++ b/functions/serverless.yml
@@ -495,6 +495,64 @@ functions:
path: /attendee_groups
method: patch
+#--------------------
+
+ MealReminder:
+ runtime: python3.11
+ handler: meal_reminder/lambda_function.lambda_handler
+ name: "${sls:stage}-meal_reminder"
+ package:
+ patterns:
+ - '!**/**'
+ - "meal_reminder/**"
+ - "shared/**"
+ environment:
+ STAGE_NAME: ${sls:stage}
+ ATTENDEES_TABLE_NAME: ${param:attendeesTableName}
+ SEND_EMAIL_LAMBDA: "${sls:stage}-send_email"
+ EVENT_TABLE_NAME: ${param:eventTableName}
+ events:
+ - httpApi:
+ path: /meal/send_reminder
+ method: post
+
+#--------------------
+
+ MealGroups:
+ runtime: python3.11
+ handler: meal_groups/lambda_function.lambda_handler
+ name: "${sls:stage}-meal_groups"
+ package:
+ patterns:
+ - '!**/**'
+ - "meal_groups/**"
+ - "shared/**"
+ environment:
+ STAGE_NAME: ${sls:stage}
+ EVENT_TABLE_NAME: ${param:eventTableName}
+ events:
+ - httpApi:
+ path: /meal/groups
+ method: get
+
+#--------------------
+ MealSummary:
+ runtime: python3.11
+ handler: meal_summary/lambda_function.lambda_handler
+ name: "${sls:stage}-meal_summary"
+ package:
+ patterns:
+ - '!**/**'
+ - "meal_summary/**"
+ - "shared/**"
+ environment:
+ STAGE_NAME: ${sls:stage}
+ EVENT_TABLE_NAME: ${param:eventTableName}
+ ATTENDEES_TABLE_NAME: ${param:attendeesTableName}
+ events:
+ - httpApi:
+ path: /meal/summary
+ method: get
diff --git a/lib/authorise.ts b/lib/authorise.ts
index 14bdb493..c8885c3e 100644
--- a/lib/authorise.ts
+++ b/lib/authorise.ts
@@ -6,7 +6,7 @@ const grantUsage = {
"developer": ["#","/admin/users","/admin/stripe","/admin/import"],
"content-manager": ['/admin/content'],
"door-staff": ['/admin/ticketing','/admin/scan', '/admin/epos'],
- "event-manager": ['/admin/ticketing.*'] // Everything under ticketing
+ "event-manager": ['/admin/ticketing.*','/admin/meal.*'] // Everything under ticketing
}
export const authUsage = (user,path) => {