From e3c05999ea98fe80cad6c57df1047bebc2b55bb2 Mon Sep 17 00:00:00 2001
From: jvcByte
+ Set a pass mark (0–100). Leave blank for no threshold.
+
+ A participant passes only if all configured constraints are met. Leave a field blank to disable that constraint.
+
+ {score !== null ? `${score.toFixed(1)}%` : '—'}
+
+```
+
+The page query (`submissions/page.tsx`) joins `sessions.score` and passes `exercise.pass_mark` as a prop.
+
+### `app/instructor/submissions/[id]/page.tsx`
+
+1. **Timestamps** — replace all `new Date(x).toLocaleString()` with `formatWAT(x)`.
+2. **Pasted text** — add a "Pasted Text" column to the paste events table.
+3. **Flag dismissal UI** — replace the static flag reasons alert with a `FlagDismissal` client component (see below).
+
+#### New client component: `FlagDismissal.tsx`
+
+```tsx
+'use client';
+// Props: submissionId, flagReasons: string[], dismissedFlags: DismissedFlag[]
+// Renders each flag reason with:
+// - Active: red badge + "Dismiss" button
+// - Dismissed: muted/strikethrough style + "Restore" button + who dismissed + when
+// On dismiss/restore: calls PUT /api/instructor/submissions/[id]/dismiss-flag
+// then updates local state (optimistic) and shows toast
+```
+
+### `app/participant/page.tsx`
+
+The query gains `sessions.score` and `exercises.pass_mark`. The exercise card for completed sessions shows:
+
+```tsx
+{sessionStatusMap.get(exercise.id) === 'completed' && (
+
+
+
+
+
+
+
+ {filtered.map((a) => {
+ const pr = (a.passed_sessions + a.failed_sessions) > 0
+ ? Math.round((a.passed_sessions / (a.passed_sessions + a.failed_sessions)) * 100)
+ : null;
+ const cr = Math.round((a.completed_sessions / a.total_sessions) * 100);
+ return (
+ Exercise
+ Sessions
+ Completed
+ Avg Score
+ Avg Qs
+ Pass / Fail
+ Flagged
+
+
+ );
+ })}
+
+
+
+ {a.title}
+
+
+ {a.total_sessions}
+
+ 50 ? 'var(--text2)' : COLORS.flagged, fontWeight: 600 }}>
+ {a.completed_sessions}
+
+ ({cr}%)
+
+ = 50 ? COLORS.passed : COLORS.failed) : 'var(--text3)' }}>
+ {a.avg_score !== null ? `${Number(a.avg_score).toFixed(1)}%` : '—'}
+
+
+ {a.avg_questions_answered !== null ? Number(a.avg_questions_answered).toFixed(1) : '—'}
+
+
+ {pr !== null ? (
+
+
+
+
+ {a.flagged_sessions > 0 ? (
+
+
+
| Participant | -Q# | -Status | +Progress | Flag | -Submitted | +Score | +Last Activity | Actions | |
|---|---|---|---|---|---|---|---|---|---|
| {sub.username} | -{sub.question_index + 1} | -- - {sub.is_final ? 'Final' : 'Draft'} - - | -
- {sub.is_flagged
- ? |
- - {new Date(sub.submitted_at).toLocaleString()} - | -- - Review - - | -||||
|
+ {p.submissions.length > 0
+ ? isExpanded ? |
+ {p.username} | ++ + {p.final_count} + /{p.total_questions} + + {p.final_count === p.total_questions && ( + Done + )} + | +
+ {p.is_flagged
+ ?
+ |
+ + {p.score !== null ? `${p.score.toFixed(1)}%` : '—'} + {passing === true && ✓} + {passing === false && ✗} + | +{p.last_submitted_at} | +e.stopPropagation()} /> + | |||
| + | + Q{sub.question_index + 1} + | ++ + {sub.is_final ? 'Final' : 'Draft'} + + | +
+ {sub.is_flagged
+ ? |
+ + | + {new Date(sub.submitted_at).toLocaleTimeString()} + | ++ + Review + + | +
Question {sub.question_index + 1} · {new Date(sub.submitted_at).toLocaleString()}
+Question {sub.question_index + 1} · {formatWAT(sub.submitted_at)}
| # | Chars Pasted | Occurred At | |||
|---|---|---|---|---|---|
| # | Chars Pasted | Pasted Text | Occurred At | ||
| {i + 1} | {ev.char_count} | -{new Date(ev.occurred_at).toLocaleString()} | ++ {ev.pasted_text ?? —} + | +{formatWAT(ev.occurred_at)} | |
| {i + 1} | -{new Date(ev.lost_at).toLocaleString()} | -{ev.regained_at ? new Date(ev.regained_at).toLocaleString() : '—'} | +{formatWAT(ev.lost_at)} | +{ev.regained_at ? formatWAT(ev.regained_at) : '—'} | {ev.duration_ms != null ? `${(ev.duration_ms / 1000).toFixed(1)}s` : '—'} | diff --git a/app/instructor/users/UserManager.tsx b/app/instructor/users/UserManager.tsx index 76d74d6..d625a17 100644 --- a/app/instructor/users/UserManager.tsx +++ b/app/instructor/users/UserManager.tsx @@ -6,6 +6,8 @@ import { toast } from 'sonner'; import { Plus, X, KeyRound, Trash2, AlertTriangle } from 'lucide-react'; import SearchInput from '@/app/components/SearchInput'; +import { formatDateWAT } from '@/lib/format'; + interface User { id: string; username: string; role: string; created_at: string; } interface Props { initialUsers: User[]; currentUserId: string; } @@ -140,7 +142,7 @@ export default function UserManager({ initialUsers, currentUserId }: Props) {
| {user.username} | {user.role} | -{new Date(user.created_at).toLocaleDateString()} | +{formatDateWAT(user.created_at)} |
|