Skip to content

Commit c0fb4a9

Browse files
committed
feat: enhance dashboard with student progress tracking and session attendance
1 parent d6e1a9c commit c0fb4a9

2 files changed

Lines changed: 119 additions & 49 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: 108 additions & 45 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]
@@ -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

10571080
async 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+
12991362
async def on_fetch(request, env):
13001363
try:
13011364
return await _dispatch(request, env)

0 commit comments

Comments
 (0)