1010 POST /api/activities – create activity [host]
1111 GET /api/activities/:id – activity + sessions + state
1212 POST /api/join – join an activity
13- GET /api/dashboard – personal dashboard
13+ GET /api/dashboard – personal dashboard with progress stats
14+ POST /api/attendance – mark session attendance [auth]
1415 POST /api/sessions – add a session to activity [host]
1516 GET /api/tags – list all tags
1617 POST /api/activity-tags – add tags to an activity [host]
@@ -993,66 +994,88 @@ async def api_dashboard(req, env):
993994 user = verify_token (req .headers .get ("Authorization" ), env .JWT_SECRET )
994995 if not user :
995996 return err ("Authentication required" , 401 )
996-
997997 enc = env .ENCRYPTION_KEY
998998
999+ # Hosted activities
9991000 res = await env .DB .prepare (
1000- "SELECT a.id,a.title,a.type,a.format,a.schedule_type,a.created_at,"
1001- "(SELECT COUNT(*) FROM enrollments WHERE activity_id=a.id AND status='active')"
1002- " AS participant_count,"
1001+ "SELECT a.id, a.title, a.type, a.format, a.schedule_type, a.created_at,"
1002+ "(SELECT COUNT(*) FROM enrollments WHERE activity_id=a.id AND status='active') AS participant_count,"
10031003 "(SELECT COUNT(*) FROM sessions WHERE activity_id=a.id) AS session_count"
10041004 " FROM activities a WHERE a.host_id=? ORDER BY a.created_at DESC"
10051005 ).bind (user ["id" ]).all ()
1006+ hosted_rows = res .results or []
1007+ hosted_ids = [r ["id" ] for r in hosted_rows ]
1008+ hosted_tags = {}
1009+ if hosted_ids :
1010+ placeholders = "," .join ("?" * len (hosted_ids ))
1011+ tag_res = await env .DB .prepare (
1012+ f"SELECT at2.activity_id, t.name FROM tags t JOIN activity_tags at2 ON at2.tag_id=t.id"
1013+ f" WHERE at2.activity_id IN ({ placeholders } )"
1014+ ).bind (* hosted_ids ).all ()
1015+ for tr in (tag_res .results or []):
1016+ hosted_tags .setdefault (tr ["activity_id" ], []).append (tr ["name" ])
1017+ hosted = [
1018+ {
1019+ "id" : r ["id" ], "title" : r ["title" ], "type" : r ["type" ],
1020+ "format" : r ["format" ], "schedule_type" : r ["schedule_type" ],
1021+ "participant_count" : r ["participant_count" ] or 0 ,
1022+ "session_count" : r ["session_count" ] or 0 ,
1023+ "tags" : hosted_tags .get (r ["id" ], []),
1024+ "created_at" : r ["created_at" ],
1025+ }
1026+ for r in hosted_rows
1027+ ]
10061028
1007- hosted = []
1008- for r in res .results or []:
1009- t_res = await env .DB .prepare (
1010- "SELECT t.name FROM tags t JOIN activity_tags at2 ON at2.tag_id=t.id"
1011- " WHERE at2.activity_id=?"
1012- ).bind (r .id ).all ()
1013- hosted .append ({
1014- "id" : r .id ,
1015- "title" : r .title ,
1016- "type" : r .type ,
1017- "format" : r .format ,
1018- "schedule_type" : r .schedule_type ,
1019- "participant_count" : r .participant_count ,
1020- "session_count" : r .session_count ,
1021- "tags" : [t .name for t in (t_res .results or [])],
1022- "created_at" : r .created_at ,
1023- })
1024-
1029+ # Joined activities with progress
10251030 res2 = await env .DB .prepare (
1026- "SELECT a.id,a.title,a.type,a.format,a.schedule_type,"
1027- "e.role AS enr_role,e.status AS enr_status,e.created_at AS joined_at,"
1028- "u.name AS host_name_enc"
1031+ "SELECT a.id, a.title, a.type, a.format, a.schedule_type,"
1032+ " e.role AS enr_role, e.status AS enr_status, e.created_at AS joined_at,"
1033+ " u.name AS host_name_enc,"
1034+ " (SELECT COUNT(*) FROM sessions WHERE activity_id=a.id) AS total_sessions,"
1035+ " (SELECT COUNT(*) FROM session_attendance sa"
1036+ " JOIN sessions s ON s.id=sa.session_id"
1037+ " WHERE s.activity_id=a.id AND sa.user_id=? AND sa.status='attended') AS attended_sessions"
10291038 " FROM enrollments e"
10301039 " JOIN activities a ON e.activity_id=a.id"
10311040 " JOIN users u ON a.host_id=u.id"
10321041 " WHERE e.user_id=? ORDER BY e.created_at DESC"
1033- ).bind (user ["id" ]).all ()
1034-
1042+ ).bind (user ["id" ], user ["id" ]).all ()
1043+ joined_rows = res2 .results or []
1044+ joined_ids = [r ["id" ] for r in joined_rows ]
1045+ joined_tags = {}
1046+ if joined_ids :
1047+ placeholders2 = "," .join ("?" * len (joined_ids ))
1048+ tag_res2 = await env .DB .prepare (
1049+ f"SELECT at2.activity_id, t.name FROM tags t JOIN activity_tags at2 ON at2.tag_id=t.id"
1050+ f" WHERE at2.activity_id IN ({ placeholders2 } )"
1051+ ).bind (* joined_ids ).all ()
1052+ for tr in (tag_res2 .results or []):
1053+ joined_tags .setdefault (tr ["activity_id" ], []).append (tr ["name" ])
10351054 joined = []
1036- for r in res2 .results or []:
1037- t_res = await env .DB .prepare (
1038- "SELECT t.name FROM tags t JOIN activity_tags at2 ON at2.tag_id=t.id"
1039- " WHERE at2.activity_id=?"
1040- ).bind (r .id ).all ()
1055+ for r in joined_rows :
1056+ total = r ["total_sessions" ] or 0
1057+ attended = r ["attended_sessions" ] or 0
1058+ progress = round ((attended / total ) * 100 ) if total > 0 else 0
10411059 joined .append ({
1042- "id" : r .id ,
1043- "title" : r .title ,
1044- "type" : r .type ,
1045- "format" : r .format ,
1046- "schedule_type" : r .schedule_type ,
1047- "enr_role" : r .enr_role ,
1048- "enr_status" : r .enr_status ,
1049- "host_name" : await decrypt_aes (r .host_name_enc or "" , enc ),
1050- "tags" : [t .name for t in (t_res .results or [])],
1051- "joined_at" : r .joined_at ,
1060+ "id" : r ["id" ], "title" : r ["title" ], "type" : r ["type" ],
1061+ "format" : r ["format" ], "schedule_type" : r ["schedule_type" ],
1062+ "enr_role" : r ["enr_role" ], "enr_status" : r ["enr_status" ],
1063+ "host_name" : decrypt (r ["host_name_enc" ] or "" , enc ),
1064+ "tags" : joined_tags .get (r ["id" ], []),
1065+ "joined_at" : r ["joined_at" ],
1066+ "total_sessions" : total ,
1067+ "attended_sessions" : attended ,
1068+ "progress_pct" : progress ,
10521069 })
10531070
1054- return json_resp ({"user" : user , "hosted_activities" : hosted , "joined_activities" : joined })
1055-
1071+ stats = {
1072+ "total_joined" : len (joined ),
1073+ "completed" : sum (1 for a in joined if a ["enr_status" ] == "completed" ),
1074+ "in_progress" : sum (1 for a in joined if a ["enr_status" ] == "active" and a ["total_sessions" ] > 0 ),
1075+ "total_sessions_attended" : sum (a ["attended_sessions" ] for a in joined ),
1076+ "hosted_count" : len (hosted ),
1077+ }
1078+ return json_resp ({"user" : user , "hosted_activities" : hosted , "joined_activities" : joined , "stats" : stats })
10561079
10571080async def api_create_session (req , env ):
10581081 user = verify_token (req .headers .get ("Authorization" ), env .JWT_SECRET )
@@ -1276,6 +1299,8 @@ async def _dispatch(request, env):
12761299 if path == "/api/join" and method == "POST" :
12771300 return await api_join (request , env )
12781301
1302+ if path == "/api/attendance" and method == "POST" :
1303+ return await api_mark_attendance (request , env )
12791304 if path == "/api/dashboard" and method == "GET" :
12801305 return await api_dashboard (request , env )
12811306
@@ -1296,6 +1321,44 @@ async def _dispatch(request, env):
12961321 return await serve_static (path , env )
12971322
12981323
1324+
1325+ async def api_mark_attendance (req , env ):
1326+ """POST /api/attendance — mark session attendance for current user."""
1327+ user = verify_token (req .headers .get ("Authorization" ), env .JWT_SECRET )
1328+ if not user :
1329+ return err ("Authentication required" , 401 )
1330+ body , bad = await parse_json_object (req )
1331+ if bad :
1332+ return bad
1333+ session_id = (body .get ("session_id" ) or "" ).strip ()
1334+ status = body .get ("status" , "registered" )
1335+ if not session_id :
1336+ return err ("session_id is required" )
1337+ if status not in ("registered" , "attended" , "missed" ):
1338+ return err ("status must be registered, attended, or missed" )
1339+ # Verify session exists and user is enrolled in that activity
1340+ sess = await env .DB .prepare (
1341+ "SELECT s.id, s.activity_id FROM sessions s WHERE s.id = ?"
1342+ ).bind (session_id ).first ()
1343+ if not sess :
1344+ return err ("Session not found" , 404 )
1345+ enr = await env .DB .prepare (
1346+ "SELECT id FROM enrollments WHERE activity_id = ? AND user_id = ? AND status = 'active'"
1347+ ).bind (sess ["activity_id" ], user ["id" ]).first ()
1348+ if not enr :
1349+ return err ("You must be enrolled in this activity" , 403 )
1350+ # Upsert attendance
1351+ try :
1352+ await env .DB .prepare (
1353+ "INSERT INTO session_attendance (id, session_id, user_id, status) VALUES (?, ?, ?, ?)"
1354+ " ON CONFLICT(session_id, user_id) DO UPDATE SET status = excluded.status"
1355+ ).bind (new_id (), session_id , user ["id" ], status ).run ()
1356+ except Exception as exc :
1357+ capture_exception (exc , req , env , where = "api_mark_attendance" )
1358+ return err ("Failed to record attendance" , 500 )
1359+ return ok ({"session_id" : session_id , "status" : status }, "Attendance recorded" )
1360+
1361+
12991362async def on_fetch (request , env ):
13001363 try :
13011364 return await _dispatch (request , env )
0 commit comments