diff --git a/apps/backoffice/src/app/features/exam/components/QuestionAssigner.tsx b/apps/backoffice/src/app/features/exam/components/QuestionAssigner.tsx index 608184138..8859a0f33 100644 --- a/apps/backoffice/src/app/features/exam/components/QuestionAssigner.tsx +++ b/apps/backoffice/src/app/features/exam/components/QuestionAssigner.tsx @@ -11,6 +11,7 @@ import { Box, Button, } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; import { IconSearch } from '@tabler/icons-react'; import type { QuestionBrief } from '../types/exam'; @@ -36,13 +37,16 @@ function QuestionList({ onSearchChange: (v: string) => void; label: string; }) { + const { t, i18n } = useTranslation(); + const locale = i18n.language as 'en' | 'am'; + const placeholder = t('exam.assigner.search'); return ( {label} ({items.length}) } value={search} onChange={(e) => onSearchChange(e.currentTarget.value)} @@ -53,7 +57,7 @@ function QuestionList({ {items.length === 0 && ( - No questions + {t('exam.assigner.noQuestions')} )} {items.map((q) => ( onToggle(q.id)} size="xs" />
- {q.title.en} + {q.title[locale]} {q.form} {q.points} pts @@ -88,11 +92,11 @@ function QuestionList({ } export function QuestionAssigner({ available, assigned, onChange, mode = 'manual' }: QuestionAssignerProps) { + const { t } = useTranslation(); const [searchLeft, setSearchLeft] = useState(''); const [searchRight, setSearchRight] = useState(''); const [selectedLeft, setSelectedLeft] = useState>(new Set()); const [selectedRight, setSelectedRight] = useState>(new Set()); - const assignedIds = new Set(assigned.map((q) => q.id)); const filteredAvailable = available.filter( @@ -115,8 +119,8 @@ export function QuestionAssigner({ available, assigned, onChange, mode = 'manual return ( - {mode === 'manual' && Assign Questions to Exam} - {mode === 'random' && Assigned Questions} + {mode === 'manual' && {t('exam.assigner.title')}} + {mode === 'random' && {t('exam.assigner.assignedTitle')}} {mode === 'manual' && ( )} {mode === 'manual' && ( {selectedLeft.size > 0 && ( )} {selectedRight.size > 0 && ( )} @@ -162,7 +166,7 @@ export function QuestionAssigner({ available, assigned, onChange, mode = 'manual {mode === 'random' && selectedRight.size > 0 && ( )} diff --git a/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx b/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx index 2f00a0d25..4e7306f5a 100644 --- a/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx +++ b/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx @@ -24,6 +24,7 @@ import { rem, } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; +import { useTranslation } from 'react-i18next'; import { IconArrowLeft, IconPrinter, @@ -66,6 +67,8 @@ function InfoRow({ label, value }: { label: string; value: string }) { } export function ExamDetailPage() { + const { t, i18n } = useTranslation(); + const locale = i18n.language as 'en' | 'am'; const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const printRef = useRef(null); @@ -94,8 +97,8 @@ export function ExamDetailPage() { if (isError || !exam) { return ( - - }>Exam not found. + + }>{t('exam.notFound')} ); } @@ -180,25 +183,20 @@ export function ExamDetailPage() { const qMap = new Map(allQuestions.map((qq) => [qq.id, qq])); const qHtml = (exam.questions ?? []).map((q, i) => { const full = qMap.get(q.id); - const titleParts = [q.title.en]; - if (q.title.am) titleParts.push(q.title.am); - const titleStr = titleParts.join(' / '); - const desc = full?.description; - const descParts: string[] = []; - if (desc?.en) descParts.push(desc.en); - if (desc?.am) descParts.push(desc.am); + const titleStr = q.title[locale] || q.title.en; + const descStr = full?.description?.[locale] || full?.description?.en || ''; return `

Question ${i + 1} (${q.points} pts — ${FORM_LABEL[q.form] ?? q.form})

${titleStr}

- ${descParts.length > 0 ? `

${descParts.join(' / ')}

` : ''} + ${descStr ? `

${descStr}

` : ''} ${q.form === 'ESSAY' ? '
'.repeat(3) : ''} ${q.form === 'CHOICE' ? ['A. ______', 'B. ______', 'C. ______', 'D. ______'].map(l => `

${l}

`).join('') : ''}
`; }).join(''); printWindow.document.write(` - ${exam.title.en} + ${exam.title[locale] || exam.title.en}
${logoBase64 ? `` : ''} -

${exam.title.en}${exam.title.am ? ' / ' + exam.title.am : ''}

+

${exam.title[locale] || exam.title.en}

Date: ${exam.date} | Venue: ${exam.venue}

Form: ${FORM_LABEL[exam.form]} | Type: ${TYPE_LABEL[exam.type]} | Time Allowed: ${exam.givenTime ? `${exam.givenTime.days}d ${exam.givenTime.hours}h ${exam.givenTime.minutes}m` : 'N/A'}

Pass Mark: ${exam.cuttingPoint} | Total Questions: ${(exam.questions ?? []).length}

- ${exam.direction?.en || exam.direction?.am ? `
Directions:${[exam.direction?.en, exam.direction?.am].filter(Boolean).join(' / ')}
` : ''} + ${exam.direction?.[locale] ? `
Directions: ${exam.direction[locale]}
` : ''} ${qHtml}
Generated by EMA — Ethiopian Maritime Authority @@ -229,7 +227,7 @@ export function ExamDetailPage() { }; const totalPoints = (exam.questions ?? []).reduce((s, q) => s + Number(q.points), 0); - const certName = exam.certification?.name?.en ?? certifications.find((c) => c.id === exam.certificationId)?.name?.en ?? '—'; + const certName = exam.certification?.name?.[locale] ?? certifications.find((c) => c.id === exam.certificationId)?.name?.[locale] ?? '—'; return ( @@ -240,50 +238,45 @@ export function ExamDetailPage() {
- {exam.title.en} - - {exam.title.am} - · - {exam.date} - + {exam.title[locale]}
{/* Status badge */} - {exam.status} + {t(`exam.status.${exam.status}`)} {/* Exam Info */} - Exam Details + {t('exam.detail.title')} - - - - - - - - - - - - + + + + + + + + + + + + - {exam.direction?.en && ( + {(exam.direction?.en || exam.direction?.am) && ( <> - + )} @@ -291,28 +284,27 @@ export function ExamDetailPage() { {/* Questions */} - Questions ({totalPoints} pts total) + {t('exam.detail.questionsSection', { pts: totalPoints })} {(exam.questions ?? []).length === 0 ? ( }> - No questions assigned yet. Click "Manage Questions" to assign. + {t('exam.noQuestionsAssigned')} ) : ( {(exam.questions ?? []).map((q, i) => ( - Question {i + 1} + {t('exam.detail.questionLabel')} {i + 1} - {FORM_LABEL[q.form] ?? q.form} + {t(`exam.formType.${q.form}`)} {q.points} pts - {q.title.en} - {q.title.am && {q.title.am}} + {q.title[locale]} ))} @@ -322,7 +314,7 @@ export function ExamDetailPage() { {/* Question assignment modal */} - + {exam.selectionMethod === 'MANUAL' ? ( <> @@ -333,18 +325,18 @@ export function ExamDetailPage() { mode="manual" /> - - + + ) : ( <> - Randomly select questions from the pool of {eligibleQuestions.length} eligible questions. The selection will automatically ensure total points meet the passing mark ({exam.cuttingPoint} pts). + {t('exam.assigner.randomHint', { total: eligibleQuestions.length, pts: exam.cuttingPoint })} setRandomCount(Number(v))} min={1} @@ -353,7 +345,7 @@ export function ExamDetailPage() { style={{ width: 80 }} /> - - + + )} diff --git a/apps/backoffice/src/app/features/exam/pages/ExamPage.tsx b/apps/backoffice/src/app/features/exam/pages/ExamPage.tsx index 8860d47dd..ab9404fe4 100644 --- a/apps/backoffice/src/app/features/exam/pages/ExamPage.tsx +++ b/apps/backoffice/src/app/features/exam/pages/ExamPage.tsx @@ -23,6 +23,7 @@ import { Divider, } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; +import { useTranslation } from 'react-i18next'; import { IconEdit, IconTrash, IconPlus, IconInfoCircle, IconCalendar, IconClipboardList } from '@tabler/icons-react'; import { notify } from '@ema-platform/ui'; import { useGetCertificationsQuery } from '../../certification/api/certification-api'; @@ -56,6 +57,7 @@ function ExamForm({ onSubmit: (values: any, isEdit: boolean) => void; onCancel: () => void; }) { + const { t } = useTranslation(); const [certificationId, setCertificationId] = useState(editing?.certificationId ?? null); const [titleEn, setTitleEn] = useState(editing?.title?.en ?? ''); const [titleAm, setTitleAm] = useState(editing?.title?.am ?? ''); @@ -89,56 +91,56 @@ function ExamForm({ return (
- - - }>Basic Info - }>Settings - + + + }>{t('exam.form.basicInfo')} + }>{t('exam.form.settings')} + - - -