Changeset 8496f3c for src/app/professor/students/page.tsx
- Timestamp:
- 09/15/26 21:16:02 (8 days ago)
- Branches:
- master
- Children:
- b8093a0
- Parents:
- 4fe4582
- File:
-
- 1 edited
-
src/app/professor/students/page.tsx (modified) (15 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/app/professor/students/page.tsx
r4fe4582 r8496f3c 3 3 import { useEffect, useMemo, useState } from "react"; 4 4 import { useTranslation } from 'react-i18next'; 5 import { apiUrl } from '@/lib/api'; 6 import { getAccessToken } from '@/lib/auth'; 5 7 6 8 type UsersBySubject = { 7 Id?: string; 8 Name?: string; 9 Grade: number; 9 /** users.id - what the grading endpoints expect as StudentId. */ 10 id: number; 11 name?: string; 12 /** users.index - the number shown to the professor and searched on. */ 13 index?: string; 14 grade: number; 15 semester?: string; 10 16 }; 11 17 12 18 type SubjectsAndUsers = { 13 Name?: string; 14 Id?: number; 15 Users: UsersBySubject[]; 19 name?: string; 20 id?: number; 21 code?: string; 22 users: UsersBySubject[]; 16 23 }; 17 24 … … 19 26 StudentId: number; 20 27 SubjectId: number; 21 grade: number;28 Grade: number; 22 29 }; 23 30 24 31 type FlatRow = { 25 studentId Str: string;26 studentI dNum: number | null;32 studentIdNum: number; 33 studentIndex: string; 27 34 studentName: string; 28 35 subjectId: number; 29 36 subjectName: string; 37 semester: string; 30 38 grade: number; 31 39 }; 32 33 function toInt(value: string): number | null {34 const n = Number.parseInt(value, 10);35 return Number.isFinite(n) ? n : null;36 }37 40 38 41 export default function ProfessorStudentsPage() { … … 52 55 setError(null); 53 56 try { 54 const res = await fetch("/api/prof/students", { cache: "no-store" }); 57 const token = getAccessToken(); 58 if (!token) { 59 throw new Error("You are not signed in."); 60 } 61 62 const res = await fetch(apiUrl("/api/prof/students"), { 63 cache: "no-store", 64 headers: { Authorization: `Bearer ${token}` }, 65 }); 55 66 if (!res.ok) { 56 67 throw new Error(`Failed to fetch students (${res.status})`); … … 61 72 const nextSelections: Record<string, number> = {}; 62 73 for (const subj of data) { 63 if (!subj. Id) continue;64 for (const u of subj. Users) {65 const key = `${subj. Id}:${u.Id ?? ""}`;66 const current = u.Grade;67 nextSelections[key] = current >= 5 && current <= 10 ? current : 5;74 if (!subj.id) continue; 75 for (const u of subj.users) { 76 const key = `${subj.id}:${u.id}`; 77 // grade_type only declares 6..10, so an ungraded row starts at 6. 78 nextSelections[key] = u.grade >= 6 && u.grade <= 10 ? u.grade : 6; 68 79 } 69 80 } … … 83 94 const rows: FlatRow[] = []; 84 95 for (const subj of subjects) { 85 if (!subj. Id) continue;86 for (const u of subj. Users) {96 if (!subj.id) continue; 97 for (const u of subj.users) { 87 98 rows.push({ 88 studentIdStr: u.Id ?? "", 89 studentIdNum: toInt(u.Id ?? ""), 90 studentName: u.Name ?? "", 91 subjectId: subj.Id, 92 subjectName: subj.Name ?? "", 93 grade: u.Grade, 99 studentIdNum: u.id, 100 studentIndex: u.index ?? "", 101 studentName: u.name ?? "", 102 subjectId: subj.id, 103 subjectName: subj.name ?? "", 104 semester: u.semester ?? "", 105 grade: u.grade, 94 106 }); 95 107 } … … 102 114 return flatRows.filter((r) => { 103 115 if (subjectFilter !== "all" && String(r.subjectId) !== subjectFilter) return false; 104 if (q.length > 0 && !r.studentI dStr.includes(q)) return false;116 if (q.length > 0 && !r.studentIndex.includes(q)) return false; 105 117 return true; 106 118 }); … … 111 123 setError(null); 112 124 try { 113 const res = await fetch(url, { 125 const token = getAccessToken(); 126 if (!token) { 127 throw new Error("You are not signed in."); 128 } 129 130 const res = await fetch(apiUrl(url), { 114 131 method: "POST", 115 headers: { "Content-Type": "application/json" }, 132 headers: { 133 "Content-Type": "application/json", 134 Authorization: `Bearer ${token}`, 135 }, 116 136 body: JSON.stringify(payload), 117 137 }); … … 135 155 <h1 className="text-2xl font-semibold text-card-foreground">{t('prof_students_title')}</h1> 136 156 <p className="text-muted-foreground mt-1"> 137 {t('prof_students_data_note')} <span className="font-mono">/api/prof/students</span> (demo store).157 {t('prof_students_data_note')} <span className="font-mono">/api/prof/students</span>. 138 158 </p> 139 159 </div> … … 149 169 <option value="all">{t('all_subjects')}</option> 150 170 {subjects 151 .filter((s) => typeof s. Id === "number")171 .filter((s) => typeof s.id === "number") 152 172 .map((s) => ( 153 <option key={String(s. Id)} value={String(s.Id)}>154 {s. Name ?? `${t('subject')} ${s.Id}`}173 <option key={String(s.id)} value={String(s.id)}> 174 {s.name ?? `${t('subject')} ${s.id}`} 155 175 </option> 156 176 ))} … … 202 222 ) : ( 203 223 filteredRows.map((r) => { 204 const key = `${r.subjectId}:${r.studentId Str}`;205 const selected = gradeSelection[key] ?? 5;224 const key = `${r.subjectId}:${r.studentIdNum}`; 225 const selected = gradeSelection[key] ?? 6; 206 226 const busy = actionBusyKey === key; 207 227 … … 209 229 <tr key={key} className="hover:bg-accent"> 210 230 <td className="px-4 py-3 border-b text-card-foreground">{r.studentName}</td> 211 <td className="px-4 py-3 border-b text-card-foreground">{r.studentIdStr}</td> 212 <td className="px-4 py-3 border-b text-card-foreground">{r.subjectName}</td> 231 <td className="px-4 py-3 border-b text-card-foreground">{r.studentIndex}</td> 232 <td className="px-4 py-3 border-b text-card-foreground"> 233 {r.subjectName} 234 {r.semester && ( 235 <span className="block text-xs text-muted-foreground">{r.semester}</span> 236 )} 237 </td> 213 238 <td className="px-4 py-3 border-b"> 214 239 <div className="flex items-center gap-3"> … … 223 248 } 224 249 > 225 {[ 5,6, 7, 8, 9, 10].map((g) => (250 {[6, 7, 8, 9, 10].map((g) => ( 226 251 <option key={g} value={g}> 227 252 {g} … … 237 262 <div className="flex flex-wrap gap-2"> 238 263 <button 239 disabled={busy || r.studentIdNum === null}264 disabled={busy} 240 265 className="px-3 py-2 rounded-lg bg-green-600 text-white text-sm font-medium disabled:opacity-50" 241 266 onClick={() => { 242 if (r.studentIdNum === null) return;243 267 void postGrade( 244 268 "/api/prof/grade/add", 245 { StudentId: r.studentIdNum, SubjectId: r.subjectId, grade: selected },269 { StudentId: r.studentIdNum, SubjectId: r.subjectId, Grade: selected }, 246 270 key, 247 271 ); … … 252 276 253 277 <button 254 disabled={busy || r.studentIdNum === null}278 disabled={busy} 255 279 className="px-3 py-2 rounded-lg bg-blue-600 text-white text-sm font-medium disabled:opacity-50" 256 280 onClick={() => { 257 if (r.studentIdNum === null) return;258 281 void postGrade( 259 282 "/api/prof/grade/edit", 260 { StudentId: r.studentIdNum, SubjectId: r.subjectId, grade: selected },283 { StudentId: r.studentIdNum, SubjectId: r.subjectId, Grade: selected }, 261 284 key, 262 285 ); … … 267 290 268 291 <button 269 disabled={busy || r.studentIdNum === null}292 disabled={busy} 270 293 className="px-3 py-2 rounded-lg bg-gray-800 text-white text-sm font-medium disabled:opacity-50" 271 294 onClick={() => { 272 if (r.studentIdNum === null) return;273 295 void postGrade( 274 296 "/api/prof/grade/remove", 275 { StudentId: r.studentIdNum, SubjectId: r.subjectId, grade: 0 },297 { StudentId: r.studentIdNum, SubjectId: r.subjectId, Grade: 0 }, 276 298 key, 277 299 );
Note:
See TracChangeset
for help on using the changeset viewer.
