104 lines
3.7 KiB
TypeScript
104 lines
3.7 KiB
TypeScript
/**
|
|
* examRepository — the SINGLE module that talks to the /api/exam backend (spec R2.1 seam).
|
|
*
|
|
* All exam-marker persistence flows through here so a later dual-write / offline cache can slot
|
|
* in without touching feature code. Mirrors the auth pattern of timetableService: take the
|
|
* Supabase session JWT and send it as a Bearer token; the API enforces RLS as the user.
|
|
*/
|
|
import axios from 'axios';
|
|
|
|
import { API_BASE } from '../../config/apiConfig';
|
|
import { supabase } from '../../supabaseClient';
|
|
import type {
|
|
BatchQueueResponse,
|
|
BatchResultsResponse,
|
|
CreateBatchPayload,
|
|
CreateTemplatePayload,
|
|
ExamTemplate,
|
|
ExamTemplateDetail,
|
|
MarkingBatch,
|
|
MarkUpsertPayload,
|
|
} from '../../types/exam.types';
|
|
|
|
const EXAM_BASE = `${API_BASE}/api/exam`;
|
|
|
|
async function authHeaders(): Promise<Record<string, string>> {
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
if (!session?.access_token) {
|
|
throw new Error('No authentication token available');
|
|
}
|
|
return { Authorization: `Bearer ${session.access_token}` };
|
|
}
|
|
|
|
export const examRepository = {
|
|
async listTemplates(includeArchived = false): Promise<ExamTemplate[]> {
|
|
const headers = await authHeaders();
|
|
const res = await axios.get<{ templates: ExamTemplate[] }>(
|
|
`${EXAM_BASE}/templates`,
|
|
{ headers, params: { include_archived: includeArchived } },
|
|
);
|
|
return res.data.templates ?? [];
|
|
},
|
|
|
|
async getTemplate(templateId: string): Promise<ExamTemplateDetail> {
|
|
const headers = await authHeaders();
|
|
const res = await axios.get<ExamTemplateDetail>(`${EXAM_BASE}/templates/${templateId}`, { headers });
|
|
return res.data;
|
|
},
|
|
|
|
async createTemplate(payload: CreateTemplatePayload): Promise<ExamTemplate> {
|
|
const headers = await authHeaders();
|
|
const res = await axios.post<ExamTemplate>(`${EXAM_BASE}/templates`, payload, { headers });
|
|
return res.data;
|
|
},
|
|
|
|
async archiveTemplate(templateId: string): Promise<void> {
|
|
const headers = await authHeaders();
|
|
await axios.delete(`${EXAM_BASE}/templates/${templateId}`, { headers });
|
|
},
|
|
|
|
async createBatch(payload: CreateBatchPayload): Promise<MarkingBatch> {
|
|
const headers = await authHeaders();
|
|
const res = await axios.post<MarkingBatch>(`${EXAM_BASE}/batches`, payload, { headers });
|
|
return res.data;
|
|
},
|
|
|
|
async listBatches(params: { includeArchived?: boolean; templateId?: string } = {}): Promise<MarkingBatch[]> {
|
|
const headers = await authHeaders();
|
|
const res = await axios.get<{ batches: MarkingBatch[] }>(`${EXAM_BASE}/batches`, {
|
|
headers,
|
|
params: {
|
|
include_archived: params.includeArchived ?? false,
|
|
template_id: params.templateId,
|
|
},
|
|
});
|
|
return res.data.batches ?? [];
|
|
},
|
|
|
|
async getBatchQueue(batchId: string): Promise<BatchQueueResponse> {
|
|
const headers = await authHeaders();
|
|
const res = await axios.get<BatchQueueResponse>(`${EXAM_BASE}/batches/${batchId}/queue`, { headers });
|
|
return res.data;
|
|
},
|
|
|
|
async getBatchResults(batchId: string): Promise<BatchResultsResponse> {
|
|
const headers = await authHeaders();
|
|
const res = await axios.get<BatchResultsResponse>(`${EXAM_BASE}/batches/${batchId}/results`, { headers });
|
|
return res.data;
|
|
},
|
|
|
|
async getBatchCsv(batchId: string): Promise<string> {
|
|
const headers = await authHeaders();
|
|
const res = await axios.get<string>(`${EXAM_BASE}/batches/${batchId}/csv`, { headers, responseType: 'text' });
|
|
return res.data;
|
|
},
|
|
|
|
async upsertMark(markId: string, payload: MarkUpsertPayload): Promise<unknown> {
|
|
const headers = await authHeaders();
|
|
const res = await axios.put(`${EXAM_BASE}/marks/${markId}`, payload, { headers });
|
|
return res.data;
|
|
},
|
|
};
|
|
|
|
export default examRepository;
|