diff --git a/public/dashboard.html b/public/dashboard.html index 47f2903..7ed185b 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -97,12 +97,12 @@

My Activiti document.getElementById('header-sub').textContent = 'Manage your hosted activities and track your participation.'; document.getElementById('section-title').textContent = 'Hosted Activities'; - const totalParts = hosted.reduce((s,a) => s + (a.participant_count||0), 0); + const stats = data.stats || {}; document.getElementById('stat-cards').innerHTML = - statCard(hosted.length, 'Hosted', 'text-brand') + - statCard(totalParts, 'Participants', 'text-purple-600') + - statCard(joined.length, 'Joined', 'text-emerald-600') + - statCard('🔒', 'Encrypted', 'text-amber-500'); + statCard(stats.hosted_count ?? hosted.length, 'Hosted', 'text-brand') + + statCard(stats.total_joined ?? joined.length, 'Joined', 'text-emerald-600') + + statCard(stats.completed ?? 0, 'Completed', 'text-blue-600') + + statCard(stats.total_sessions_attended ?? 0, 'Sessions Attended','text-purple-600'); document.getElementById('quick-actions').innerHTML = '
' + @@ -150,6 +150,11 @@

My Activiti const rc = roleColor[a.enr_role] || 'bg-slate-100 text-slate-600'; const sc = statusColor[a.enr_status] || 'bg-slate-100 text-slate-600'; const tags = (a.tags||[]).slice(0,3).map(t => '' + esc(t) + '').join(''); + const pct = a.progress_pct ?? 0; + const progressBar = a.total_sessions > 0 + ? '
Progress' + a.attended_sessions + '/' + a.total_sessions + ' sessions
' + + '
' + : '

No sessions scheduled yet

'; return '
' + '
' + '

' + ic + ' ' + esc(a.title) + '

' + @@ -161,6 +166,7 @@

My Activiti '' + esc(a.enr_status) + '' + '' + (fmtLabel[a.format]||a.format) + '' + '

' + + progressBar + '
' + tags + '
' + 'View Activity' + '
'; diff --git a/src/worker.py b/src/worker.py index a0296a6..91c70f1 100644 --- a/src/worker.py +++ b/src/worker.py @@ -10,7 +10,8 @@ POST /api/activities – create activity [host] GET /api/activities/:id – activity + sessions + state POST /api/join – join an activity - GET /api/dashboard – personal dashboard + GET /api/dashboard – personal dashboard with progress stats + POST /api/attendance – mark session attendance [auth] POST /api/sessions – add a session to activity [host] GET /api/tags – list all tags POST /api/activity-tags – add tags to an activity [host] @@ -993,66 +994,88 @@ async def api_dashboard(req, env): user = verify_token(req.headers.get("Authorization"), env.JWT_SECRET) if not user: return err("Authentication required", 401) - enc = env.ENCRYPTION_KEY + # Hosted activities res = await env.DB.prepare( - "SELECT a.id,a.title,a.type,a.format,a.schedule_type,a.created_at," - "(SELECT COUNT(*) FROM enrollments WHERE activity_id=a.id AND status='active')" - " AS participant_count," + "SELECT a.id, a.title, a.type, a.format, a.schedule_type, a.created_at," + "(SELECT COUNT(*) FROM enrollments WHERE activity_id=a.id AND status='active') AS participant_count," "(SELECT COUNT(*) FROM sessions WHERE activity_id=a.id) AS session_count" " FROM activities a WHERE a.host_id=? ORDER BY a.created_at DESC" ).bind(user["id"]).all() + hosted_rows = res.results or [] + hosted_ids = [r["id"] for r in hosted_rows] + hosted_tags = {} + if hosted_ids: + placeholders = ",".join("?" * len(hosted_ids)) + tag_res = await env.DB.prepare( + f"SELECT at2.activity_id, t.name FROM tags t JOIN activity_tags at2 ON at2.tag_id=t.id" + f" WHERE at2.activity_id IN ({placeholders})" + ).bind(*hosted_ids).all() + for tr in (tag_res.results or []): + hosted_tags.setdefault(tr["activity_id"], []).append(tr["name"]) + hosted = [ + { + "id": r["id"], "title": r["title"], "type": r["type"], + "format": r["format"], "schedule_type": r["schedule_type"], + "participant_count": r["participant_count"] or 0, + "session_count": r["session_count"] or 0, + "tags": hosted_tags.get(r["id"], []), + "created_at": r["created_at"], + } + for r in hosted_rows + ] - hosted = [] - for r in res.results or []: - t_res = await env.DB.prepare( - "SELECT t.name FROM tags t JOIN activity_tags at2 ON at2.tag_id=t.id" - " WHERE at2.activity_id=?" - ).bind(r.id).all() - hosted.append({ - "id": r.id, - "title": r.title, - "type": r.type, - "format": r.format, - "schedule_type": r.schedule_type, - "participant_count": r.participant_count, - "session_count": r.session_count, - "tags": [t.name for t in (t_res.results or [])], - "created_at": r.created_at, - }) - + # Joined activities with progress res2 = await env.DB.prepare( - "SELECT a.id,a.title,a.type,a.format,a.schedule_type," - "e.role AS enr_role,e.status AS enr_status,e.created_at AS joined_at," - "u.name AS host_name_enc" + "SELECT a.id, a.title, a.type, a.format, a.schedule_type," + " e.role AS enr_role, e.status AS enr_status, e.created_at AS joined_at," + " u.name AS host_name_enc," + " (SELECT COUNT(*) FROM sessions WHERE activity_id=a.id) AS total_sessions," + " (SELECT COUNT(*) FROM session_attendance sa" + " JOIN sessions s ON s.id=sa.session_id" + " WHERE s.activity_id=a.id AND sa.user_id=? AND sa.status='attended') AS attended_sessions" " FROM enrollments e" " JOIN activities a ON e.activity_id=a.id" " JOIN users u ON a.host_id=u.id" " WHERE e.user_id=? ORDER BY e.created_at DESC" - ).bind(user["id"]).all() - + ).bind(user["id"], user["id"]).all() + joined_rows = res2.results or [] + joined_ids = [r["id"] for r in joined_rows] + joined_tags = {} + if joined_ids: + placeholders2 = ",".join("?" * len(joined_ids)) + tag_res2 = await env.DB.prepare( + f"SELECT at2.activity_id, t.name FROM tags t JOIN activity_tags at2 ON at2.tag_id=t.id" + f" WHERE at2.activity_id IN ({placeholders2})" + ).bind(*joined_ids).all() + for tr in (tag_res2.results or []): + joined_tags.setdefault(tr["activity_id"], []).append(tr["name"]) joined = [] - for r in res2.results or []: - t_res = await env.DB.prepare( - "SELECT t.name FROM tags t JOIN activity_tags at2 ON at2.tag_id=t.id" - " WHERE at2.activity_id=?" - ).bind(r.id).all() + for r in joined_rows: + total = r["total_sessions"] or 0 + attended = r["attended_sessions"] or 0 + progress = round((attended / total) * 100) if total > 0 else 0 joined.append({ - "id": r.id, - "title": r.title, - "type": r.type, - "format": r.format, - "schedule_type": r.schedule_type, - "enr_role": r.enr_role, - "enr_status": r.enr_status, - "host_name": await decrypt_aes(r.host_name_enc or "", enc), - "tags": [t.name for t in (t_res.results or [])], - "joined_at": r.joined_at, + "id": r["id"], "title": r["title"], "type": r["type"], + "format": r["format"], "schedule_type": r["schedule_type"], + "enr_role": r["enr_role"], "enr_status": r["enr_status"], + "host_name": await decrypt_aes(r["host_name_enc"] or "", enc), + "tags": joined_tags.get(r["id"], []), + "joined_at": r["joined_at"], + "total_sessions": total, + "attended_sessions": attended, + "progress_pct": progress, }) - return json_resp({"user": user, "hosted_activities": hosted, "joined_activities": joined}) - + stats = { + "total_joined": len(joined), + "completed": sum(1 for a in joined if a["enr_status"] == "completed"), + "in_progress": sum(1 for a in joined if a["enr_status"] == "active" and a["total_sessions"] > 0), + "total_sessions_attended": sum(a["attended_sessions"] for a in joined), + "hosted_count": len(hosted), + } + return json_resp({"user": user, "hosted_activities": hosted, "joined_activities": joined, "stats": stats}) async def api_create_session(req, env): user = verify_token(req.headers.get("Authorization"), env.JWT_SECRET) @@ -1276,6 +1299,8 @@ async def _dispatch(request, env): if path == "/api/join" and method == "POST": return await api_join(request, env) + if path == "/api/attendance" and method == "POST": + return await api_mark_attendance(request, env) if path == "/api/dashboard" and method == "GET": return await api_dashboard(request, env) @@ -1296,6 +1321,44 @@ async def _dispatch(request, env): return await serve_static(path, env) + +async def api_mark_attendance(req, env): + """POST /api/attendance — mark session attendance for current user.""" + user = verify_token(req.headers.get("Authorization"), env.JWT_SECRET) + if not user: + return err("Authentication required", 401) + body, bad = await parse_json_object(req) + if bad: + return bad + session_id = (body.get("session_id") or "").strip() + status = body.get("status", "registered") + if not session_id: + return err("session_id is required") + if status not in ("registered", "attended", "missed"): + return err("status must be registered, attended, or missed") + # Verify session exists and user is enrolled in that activity + sess = await env.DB.prepare( + "SELECT s.id, s.activity_id FROM sessions s WHERE s.id = ?" + ).bind(session_id).first() + if not sess: + return err("Session not found", 404) + enr = await env.DB.prepare( + "SELECT id FROM enrollments WHERE activity_id = ? AND user_id = ? AND status = 'active'" + ).bind(sess["activity_id"], user["id"]).first() + if not enr: + return err("You must be enrolled in this activity", 403) + # Upsert attendance + try: + await env.DB.prepare( + "INSERT INTO session_attendance (id, session_id, user_id, status) VALUES (?, ?, ?, ?)" + " ON CONFLICT(session_id, user_id) DO UPDATE SET status = excluded.status" + ).bind(new_id(), session_id, user["id"], status).run() + except Exception as exc: + capture_exception(exc, req, env, where="api_mark_attendance") + return err("Failed to record attendance", 500) + return ok({"session_id": session_id, "status": status}, "Attendance recorded") + + async def on_fetch(request, env): try: return await _dispatch(request, env)