diff --git a/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx b/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx index 89187b097..c9de1a010 100644 --- a/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx +++ b/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useMemo } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { Stack, @@ -39,10 +39,12 @@ import { IconX, } from '@tabler/icons-react'; import { notify } from '@ema-platform/ui'; -import { useApiQuery } from '@ema-platform/api'; -import { useGetExamQuery, useUpdateExamMutation } from '../api/exam-api'; -import { useCreateResultMutation } from '../../result/api/result-api'; -import type { Exam, ExamStatus } from '../types/exam'; +import { useGetExamQuery, useUpdateExamMutation, useAssignQuestionsMutation } from '../api/exam-api'; +import { useGetQuestionsQuery } from '../../question/api/question-api'; +import { useGetCertificationsQuery } from '../../certification/api/certification-api'; +import { QuestionAssigner } from '../components/QuestionAssigner'; +import { RecordResultModal } from '../../result/components/RecordResultModal'; +import type { ExamStatus, QuestionBrief } from '../types/exam'; const STATUS_COLOR: Record = { PENDING: 'gray', ACTIVE: 'blue', COMPLETED: 'teal', @@ -63,177 +65,30 @@ function InfoRow({ label, value }: { label: string; value: string }) { ); } -function RecordResultModal({ - exam, - opened, - onClose, -}: { - exam: Exam; - opened: boolean; - onClose: () => void; -}) { - const [seafarerSearch, setSeafarerSearch] = useState(''); - const [selectedSeafarerId, setSelectedSeafarerId] = useState(null); - const [scores, setScores] = useState>({}); - const [remark, setRemark] = useState(''); - - const { data: profilesRes } = useApiQuery<{ total: number; items: any[] }>({ - url: '/profiles', - params: { q: 'w=type:=:SEAFARER' }, - }); - const [createResult, { isLoading: isSaving }] = useCreateResultMutation(); - - const seafarers = profilesRes?.items ?? []; - const questions = exam.questions ?? []; - - const seafarerOptions = seafarers.map((s: any) => ({ - value: s.id, - label: `${s.firstName} ${s.middleName ?? ''} ${s.lastName}`, - })); - - const filteredOptions = seafarerSearch - ? seafarerOptions.filter((o: any) => o.label.toLowerCase().includes(seafarerSearch.toLowerCase())) - : seafarerOptions; - - const totalScore = questions.reduce((sum, q) => sum + (scores[q.id] ?? 0), 0); - const passed = totalScore >= exam.cuttingPoint; - - const handleScoreChange = (questionId: string, value: number) => { - setScores((prev) => ({ ...prev, [questionId]: value })); - }; - - const handleSave = async () => { - if (!selectedSeafarerId) { - notify.error('Please select a seafarer'); - return; - } - try { - const breakdowns = questions.map((q) => ({ - questionId: q.id, - score: scores[q.id] ?? 0, - remark: '', - })); - await createResult({ - seafarerId: selectedSeafarerId, - examId: exam.id, - resultBreakdowns: breakdowns, - totalScore, - remark: remark ? { en: remark, am: '' } : undefined, - }).unwrap(); - notify.success(`Result recorded — ${passed ? 'PASSED' : 'FAILED'} (${totalScore}/${exam.cuttingPoint})`); - setSelectedSeafarerId(null); - setScores({}); - setRemark(''); - setSeafarerSearch(''); - onClose(); - } catch { - notify.error('Failed to save result'); - } - }; - - return ( - - - { + setSelectedSeafarerId(v); + setScores({}); + setQuestionRemarks({}); + }} + searchable + onSearchChange={setSeafarerSearch} + size="sm" + required + /> + + {selectedSeafarerId && questions.length > 0 && ( + <> + + + + + Question + Form + Max Points + Score + Remark + + + + {questions.map((q) => ( + + {q.title.en} + + {q.form} + + {q.points} + + handleScoreChange(q.id, Number(v))} + min={0} + max={q.points} + size="xs" + style={{ width: 80 }} + /> + + + handleQuestionRemarkChange(q.id, e.currentTarget.value)} + size="xs" + style={{ minWidth: 160 }} + /> + + + ))} + +
+ + + + + +
+ Status + + {passed + ? <>PASSED + : <>FAILED} + +
+
+
+ + setRemark(e.currentTarget.value)} + size="sm" + /> + + + + + + + )} + + {selectedSeafarerId && questions.length === 0 && ( + }> + No questions assigned to this exam. Assign questions first. + + )} +
+
+ ); +} diff --git a/apps/backoffice/src/app/features/result/pages/ResultPage.tsx b/apps/backoffice/src/app/features/result/pages/ResultPage.tsx index b527d8a1b..d6909653e 100644 --- a/apps/backoffice/src/app/features/result/pages/ResultPage.tsx +++ b/apps/backoffice/src/app/features/result/pages/ResultPage.tsx @@ -31,11 +31,14 @@ import { IconCalendar, IconClock, IconMapPin, + IconPlus, } from '@tabler/icons-react'; import { notify } from '@ema-platform/ui'; import { useGetResultsQuery, useLazyGetResultQuery, useDeleteResultMutation } from '../api/result-api'; import { useGetExamsQuery } from '../../exam/api/exam-api'; +import { RecordResultModal } from '../components/RecordResultModal'; import type { Result } from '../types/result'; +import type { Exam } from '../../exam/types/exam'; const STATUS_COLOR: Record = { PASSED: 'teal', @@ -213,9 +216,30 @@ export function ResultPage() { const [detailOpened, { open: openDetail, close: closeDetail }] = useDisclosure(false); const [deleteTarget, setDeleteTarget] = useState(null); const [deleteOpened, { open: openDelete, close: closeDelete }] = useDisclosure(false); + const [pickerExamId, setPickerExamId] = useState(null); + const [pickerOpened, { open: openPicker, close: closePicker }] = useDisclosure(false); + const [recordExam, setRecordExam] = useState(null); + const [recordOpened, { open: openRecord, close: closeRecord }] = useDisclosure(false); const examOptions = exams.map((e) => ({ value: e.id, label: `${e.title.en} (${e.date})` })); + const startRecord = () => { + const ex = exams.find((e) => e.id === pickerExamId); + if (!ex) { + notify.error('Please select an exam'); + return; + } + setRecordExam(ex); + closePicker(); + openRecord(); + }; + + const handleRecordClose = () => { + closeRecord(); + setRecordExam(null); + setPickerExamId(null); + }; + const filtered = results.filter((r) => !examFilter || r.examId === examFilter); const getExamTitle = (id: string) => exams.find((e) => e.id === id)?.title?.en ?? '-'; @@ -242,10 +266,15 @@ export function ResultPage() { return ( -
- Exam Results - View seafarer examination results and score breakdowns -
+ +
+ Exam Results + View seafarer examination results and score breakdowns +
+ +
@@ -343,6 +372,31 @@ export function ResultPage() { + + {/* Choose exam, then record */} + + + Choose the exam you want to record a result for. +