Skip to content

Commit 85d9421

Browse files
committed
feat: enhance dashboard with student progress tracking and session attendance
1 parent a08bafc commit 85d9421

2 files changed

Lines changed: 98 additions & 42 deletions

File tree

public/dashboard.html

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,13 @@ <h2 class="text-xl font-bold text-slate-800 mb-5" id="section-title">My Activiti
9797
document.getElementById('header-sub').textContent = 'Manage your hosted activities and track your participation.';
9898
document.getElementById('section-title').textContent = 'Hosted Activities';
9999

100+
const stats = data.stats || {};
100101
const totalParts = hosted.reduce((s,a) => s + (a.participant_count||0), 0);
101102
document.getElementById('stat-cards').innerHTML =
102-
statCard(hosted.length, 'Hosted', 'text-brand') +
103-
statCard(totalParts, 'Participants', 'text-purple-600') +
104-
statCard(joined.length, 'Joined', 'text-emerald-600') +
105-
statCard('🔒', 'Encrypted', 'text-amber-500');
103+
statCard(stats.hosted_count ?? hosted.length, 'Hosted', 'text-brand') +
104+
statCard(stats.total_joined ?? joined.length, 'Joined', 'text-emerald-600') +
105+
statCard(stats.completed ?? 0, 'Completed', 'text-blue-600') +
106+
statCard(stats.total_sessions_attended ?? 0, 'Sessions Attended','text-purple-600');
106107

107108
document.getElementById('quick-actions').innerHTML =
108109
'<div class="flex flex-wrap gap-3">' +
@@ -150,6 +151,11 @@ <h2 class="text-xl font-bold text-slate-800 mb-5" id="section-title">My Activiti
150151
const rc = roleColor[a.enr_role] || 'bg-slate-100 text-slate-600';
151152
const sc = statusColor[a.enr_status] || 'bg-slate-100 text-slate-600';
152153
const tags = (a.tags||[]).slice(0,3).map(t => '<span class="badge bg-slate-100 text-slate-500">' + esc(t) + '</span>').join('');
154+
const pct = a.progress_pct ?? 0;
155+
const progressBar = a.total_sessions > 0
156+
? '<div class="mt-1"><div class="flex justify-between text-xs text-slate-400 mb-1"><span>Progress</span><span>' + a.attended_sessions + '/' + a.total_sessions + ' sessions</span></div>' +
157+
'<div class="w-full bg-slate-100 rounded-full h-2" role="progressbar" aria-valuenow="' + pct + '" aria-valuemin="0" aria-valuemax="100" aria-label="Session attendance progress"><div class="bg-indigo-500 h-2 rounded-full transition-all" style="width:' + pct + '%"></div></div></div>'
158+
: '<p class="text-xs text-slate-400">No sessions scheduled yet</p>';
153159
return '<article class="bg-white rounded-2xl shadow-sm border border-slate-100 p-5 card-hover flex flex-col gap-3">' +
154160
'<div class="flex items-start justify-between gap-2">' +
155161
'<h3 class="font-bold text-slate-800 text-sm">' + ic + ' ' + esc(a.title) + '</h3>' +
@@ -161,6 +167,7 @@ <h2 class="text-xl font-bold text-slate-800 mb-5" id="section-title">My Activiti
161167
'<span class="badge ' + sc + '">' + esc(a.enr_status) + '</span>' +
162168
'<span class="text-xs text-slate-400">' + (fmtLabel[a.format]||a.format) + '</span>' +
163169
'</div>' +
170+
progressBar +
164171
'<div class="flex flex-wrap gap-1">' + tags + '</div>' +
165172
'<a href="/course.html?id=' + esc(a.id) + '" class="mt-auto block text-center text-sm font-semibold text-brand border border-brand/30 rounded-lg py-1.5 hover:bg-indigo-50">View Activity</a>' +
166173
'</article>';

src/worker.py

Lines changed: 87 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
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

964970
async 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+
12061255
async def on_fetch(request, env):
12071256
try:
12081257
return await _dispatch(request, env)

0 commit comments

Comments
 (0)