Compare commits

..
Author SHA1 Message Date
kcar 7a01b3e8f6 Merge remote-tracking branch 'origin/master' into agent/s4-11-marking-results
# Conflicts:
#	src/AppRoutes.tsx
#	src/pages/exam/index.ts
#	src/services/exam/examRepository.ts
#	src/types/exam.types.ts
2026-06-07 19:58:38 +01:00
kcar bff91a4b17 feat(exam): S4-11 marking flow and class results 2026-06-07 19:50:20 +01:00
3 changed files with 16 additions and 31 deletions
+1
View File
@@ -169,6 +169,7 @@ const AppRoutes: React.FC = () => {
<Route path="/classes" element={<ClassesPage />} />
<Route path="/my-classes" element={<MyClassesPage />} />
<Route path="/classes/:classId" element={<ClassDetailPage />} />
<Route path="/timetable/classes/:classId" element={<ClassDetailPage />} />
<Route path="/student-lessons" element={<StudentLessonsPage />} />
<Route path="/lesson-plans" element={<LessonPlansPage />} />
<Route path="/lesson-plans/:planId" element={<LessonPlanDetailPage />} />
+15 -5
View File
@@ -132,10 +132,20 @@ const ClassDetailPage: React.FC = () => {
setLoading(true);
setError(null);
try {
const clsRes = await fetch(`${API_BASE}/classes/${classId}`, {
const clsRes = await fetch(`${API_BASE}/database/timetable/classes/${classId}`, {
headers: { Authorization: `Bearer ${accessToken}` },
}).then(r => r.json());
if (clsRes.id) setCls(clsRes);
if (clsRes.id) {
setCls({
...clsRes,
class_code: clsRes.class_code || clsRes.code,
year_group: clsRes.year_group || clsRes.school_year,
teachers: clsRes.teachers || [],
students: clsRes.students || [],
enrollment_requests: clsRes.enrollment_requests || [],
student_count: clsRes.student_count ?? clsRes.students?.length ?? 0,
});
}
else setError(clsRes.detail || 'Class not found');
const role = bootstrapData?.active_institute?.membership_role || '';
setIsAdmin(role === 'school_admin' || role === 'department_head');
@@ -175,20 +185,20 @@ const ClassDetailPage: React.FC = () => {
const handleAddStudent = async (studentId: string) => {
setActionError(null);
const res = await apiPost(`/classes/${classId}/students`, { student_id: studentId });
const res = await apiPost(`/database/timetable/classes/${classId}/students`, { student_id: studentId });
if (res.status === 'ok') load();
else setActionError(res.detail || 'Failed to add student');
};
const handleRemoveStudent = async (studentId: string) => {
setActionError(null);
await apiDelete(`/classes/${classId}/students/${studentId}`);
await apiDelete(`/database/timetable/classes/${classId}/students/${studentId}`);
load();
};
const handleEnrollmentResponse = async (requestId: string, action: 'approve' | 'reject') => {
setActionError(null);
const res = await apiPatch(`/classes/${classId}/enrollment-requests/${requestId}`, { action });
const res = await apiPatch(`/database/timetable/classes/${classId}/enrollment-requests/${requestId}`, { action });
if (res.status === 'ok') load();
else setActionError(res.detail || 'Action failed');
};
-26
View File
@@ -62,30 +62,4 @@ describe('exam setup canvas serialization', () => {
expect(shapes.map((s) => s.kind).sort()).toEqual(['boundary', 'furniture', 'part', 'response'])
expect(shapes.find((s) => s.kind === 'part')).toMatchObject({ id: 'p1', x: 1, y: 2, w: 3, h: 4 })
})
it('round-trips boundary y as page-local with page_index while rendering at full page width', () => {
const pages = [
{ pageNumber: 1, x: 0, y: 0, w: 780, h: 1000 },
{ pageNumber: 2, x: 0, y: 1000, w: 820, h: 1200 },
]
const shapes = shapesFromTemplate({
...template,
page_count: 2,
boundaries: [
{ id: 'b1', template_id: 'tpl-1', question_id: 'q1', label: 'Q1 start', page_index: 1, y: 160, bounds: { x: 48, y: 160, w: 700, h: 8 }, source: 'manual', confirmed: true },
{ id: 'b2', template_id: 'tpl-1', question_id: 'q1', label: 'Q1 end', page_index: 1, y: 720, bounds: null, source: 'manual', confirmed: true },
],
}, pages)
expect(shapes[0]).toMatchObject({ kind: 'boundary', x: 0, y: 1160, w: 820, h: 8 })
expect(shapes[1]).toMatchObject({ kind: 'boundary', x: 0, y: 1720, w: 820, h: 8 })
const payload = serializeCanvasShapes(template, shapes, pages)
expect(payload.boundaries.map((b) => ({ page_index: b.page_index, y: b.y }))).toEqual([
{ page_index: 1, y: 160 },
{ page_index: 1, y: 720 },
])
expect(payload.boundaries.every((b) => b.bounds.w === 820)).toBe(true)
})
})