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]
@@ -900,66 +901,71 @@ async def api_dashboard(req, env):
900901 user = verify_token (req .headers .get ("Authorization" ), env .JWT_SECRET )
901902 if not user :
902903 return err ("Authentication required" , 401 )
903-
904904 enc = env .ENCRYPTION_KEY
905905
906+ # Hosted activities
906907 res = await env .DB .prepare (
907- "SELECT a.id,a.title,a.type,a.format,a.schedule_type,a.created_at,"
908- "(SELECT COUNT(*) FROM enrollments WHERE activity_id=a.id AND status='active')"
909- " AS participant_count,"
908+ "SELECT a.id, a.title, a.type, a.format, a.schedule_type, a.created_at,"
909+ "(SELECT COUNT(*) FROM enrollments WHERE activity_id=a.id AND status='active') AS participant_count,"
910910 "(SELECT COUNT(*) FROM sessions WHERE activity_id=a.id) AS session_count"
911911 " FROM activities a WHERE a.host_id=? ORDER BY a.created_at DESC"
912912 ).bind (user ["id" ]).all ()
913-
914913 hosted = []
915914 for r in res .results or []:
916915 t_res = await env .DB .prepare (
917- "SELECT t.name FROM tags t JOIN activity_tags at2 ON at2.tag_id=t.id"
918- " WHERE at2.activity_id=?"
919- ).bind (r .id ).all ()
916+ "SELECT t.name FROM tags t JOIN activity_tags at2 ON at2.tag_id=t.id WHERE at2.activity_id=?"
917+ ).bind (r ["id" ]).all ()
920918 hosted .append ({
921- "id" : r .id ,
922- "title" : r .title ,
923- "type" : r .type ,
924- "format" : r .format ,
925- "schedule_type" : r .schedule_type ,
926- "participant_count" : r .participant_count ,
927- "session_count" : r .session_count ,
928- "tags" : [t .name for t in (t_res .results or [])],
929- "created_at" : r .created_at ,
919+ "id" : r ["id" ], "title" : r ["title" ], "type" : r ["type" ],
920+ "format" : r ["format" ], "schedule_type" : r ["schedule_type" ],
921+ "participant_count" : r ["participant_count" ] or 0 ,
922+ "session_count" : r ["session_count" ] or 0 ,
923+ "tags" : [t ["name" ] for t in (t_res .results or [])],
924+ "created_at" : r ["created_at" ],
930925 })
931926
927+ # Joined activities with progress
932928 res2 = await env .DB .prepare (
933- "SELECT a.id,a.title,a.type,a.format,a.schedule_type,"
934- "e.role AS enr_role,e.status AS enr_status,e.created_at AS joined_at,"
935- "u.name AS host_name_enc"
929+ "SELECT a.id, a.title, a.type, a.format, a.schedule_type,"
930+ " e.role AS enr_role, e.status AS enr_status, e.created_at AS joined_at,"
931+ " u.name AS host_name_enc,"
932+ " (SELECT COUNT(*) FROM sessions WHERE activity_id=a.id) AS total_sessions,"
933+ " (SELECT COUNT(*) FROM session_attendance sa"
934+ " JOIN sessions s ON s.id=sa.session_id"
935+ " WHERE s.activity_id=a.id AND sa.user_id=? AND sa.status='attended') AS attended_sessions"
936936 " FROM enrollments e"
937937 " JOIN activities a ON e.activity_id=a.id"
938938 " JOIN users u ON a.host_id=u.id"
939939 " WHERE e.user_id=? ORDER BY e.created_at DESC"
940- ).bind (user ["id" ]).all ()
941-
940+ ).bind (user ["id" ], user ["id" ]).all ()
942941 joined = []
943942 for r in res2 .results or []:
944943 t_res = await env .DB .prepare (
945- "SELECT t.name FROM tags t JOIN activity_tags at2 ON at2.tag_id=t.id"
946- " WHERE at2.activity_id=?"
947- ).bind (r .id ).all ()
944+ "SELECT t.name FROM tags t JOIN activity_tags at2 ON at2.tag_id=t.id WHERE at2.activity_id=?"
945+ ).bind (r ["id" ]).all ()
946+ total = r ["total_sessions" ] or 0
947+ attended = r ["attended_sessions" ] or 0
948+ progress = round ((attended / total ) * 100 ) if total > 0 else 0
948949 joined .append ({
949- "id" : r .id ,
950- "title" : r .title ,
951- "type" : r .type ,
952- "format" : r .format ,
953- "schedule_type" : r .schedule_type ,
954- "enr_role" : r .enr_role ,
955- "enr_status" : r .enr_status ,
956- "host_name" : decrypt (r .host_name_enc or "" , enc ),
957- "tags" : [t .name for t in (t_res .results or [])],
958- "joined_at" : r .joined_at ,
950+ "id" : r ["id" ], "title" : r ["title" ], "type" : r ["type" ],
951+ "format" : r ["format" ], "schedule_type" : r ["schedule_type" ],
952+ "enr_role" : r ["enr_role" ], "enr_status" : r ["enr_status" ],
953+ "host_name" : decrypt (r ["host_name_enc" ] or "" , enc ),
954+ "tags" : [t ["name" ] for t in (t_res .results or [])],
955+ "joined_at" : r ["joined_at" ],
956+ "total_sessions" : total ,
957+ "attended_sessions" : attended ,
958+ "progress_pct" : progress ,
959959 })
960960
961- return json_resp ({"user" : user , "hosted_activities" : hosted , "joined_activities" : joined })
962-
961+ stats = {
962+ "total_joined" : len (joined ),
963+ "completed" : sum (1 for a in joined if a ["enr_status" ] == "completed" ),
964+ "in_progress" : sum (1 for a in joined if a ["enr_status" ] == "active" and a ["total_sessions" ] > 0 ),
965+ "total_sessions_attended" : sum (a ["attended_sessions" ] for a in joined ),
966+ "hosted_count" : len (hosted ),
967+ }
968+ return json_resp ({"user" : user , "hosted_activities" : hosted , "joined_activities" : joined , "stats" : stats })
963969
964970async def api_create_session (req , env ):
965971 user = verify_token (req .headers .get ("Authorization" ), env .JWT_SECRET )
@@ -1183,6 +1189,8 @@ async def _dispatch(request, env):
11831189 if path == "/api/join" and method == "POST" :
11841190 return await api_join (request , env )
11851191
1192+ if path == "/api/attendance" and method == "POST" :
1193+ return await api_mark_attendance (request , env )
11861194 if path == "/api/dashboard" and method == "GET" :
11871195 return await api_dashboard (request , env )
11881196
@@ -1203,6 +1211,47 @@ async def _dispatch(request, env):
12031211 return await serve_static (path , env )
12041212
12051213
1214+
1215+ async def api_mark_attendance (req , env ):
1216+ """POST /api/attendance — mark session attendance for current user."""
1217+ user = verify_token (req .headers .get ("Authorization" ), env .JWT_SECRET )
1218+ if not user :
1219+ return err ("Authentication required" , 401 )
1220+ body , bad = await parse_json_object (req )
1221+ if bad :
1222+ return bad
1223+ session_id = (body .get ("session_id" ) or "" ).strip ()
1224+ status = body .get ("status" , "attended" )
1225+ if not session_id :
1226+ return err ("session_id is required" )
1227+ if status not in ("registered" , "attended" , "missed" ):
1228+ return err ("status must be registered, attended, or missed" )
1229+ # Verify session exists and user is enrolled in that activity
1230+ sess = await env .DB .prepare (
1231+ "SELECT s.id, s.activity_id FROM sessions s WHERE s.id = ?"
1232+ ).bind (session_id ).first ()
1233+ if not sess :
1234+ return err ("Session not found" , 404 )
1235+ enr = await env .DB .prepare (
1236+ "SELECT id FROM enrollments WHERE activity_id = ? AND user_id = ? AND status = 'active'"
1237+ ).bind (sess ["activity_id" ], user ["id" ]).first ()
1238+ is_host = await env .DB .prepare (
1239+ "SELECT id FROM activities WHERE id = ? AND host_id = ?"
1240+ ).bind (sess ["activity_id" ], user ["id" ]).first ()
1241+ if not enr and not is_host :
1242+ return err ("You must be enrolled in this activity" , 403 )
1243+ # Upsert attendance
1244+ try :
1245+ await env .DB .prepare (
1246+ "INSERT INTO session_attendance (id, session_id, user_id, status) VALUES (?, ?, ?, ?)"
1247+ " ON CONFLICT(session_id, user_id) DO UPDATE SET status = excluded.status"
1248+ ).bind (new_id (), session_id , user ["id" ], status ).run ()
1249+ except Exception as exc :
1250+ capture_exception (exc , where = "api_mark_attendance" )
1251+ return err ("Failed to record attendance" , 500 )
1252+ return ok ({"session_id" : session_id , "status" : status }, "Attendance recorded" )
1253+
1254+
12061255async def on_fetch (request , env ):
12071256 try :
12081257 return await _dispatch (request , env )
0 commit comments