From 7c968c70935c034b7475dbaeccb1f12ba491e875 Mon Sep 17 00:00:00 2001 From: Mulu Mehari Date: Fri, 7 Aug 2026 11:50:07 +0300 Subject: [PATCH] Adding more functionalities for all the license types --- .../features/coc-queue/pages/CoCQueuePage.tsx | 21 - .../coc-queue/pages/CoCReviewPage.tsx | 21 - .../src/app/features/exam/api/exam-api.ts | 55 ++ .../exam/components/ExamCandidatesPanel.tsx | 215 +++++ .../exam/components/ExamIncidentsPanel.tsx | 300 +++++++ .../features/exam/pages/ExamDetailPage.tsx | 94 +-- .../src/app/features/exam/types/exam.ts | 80 ++ .../pages/LicenseRegisterPage.tsx | 273 +++++++ .../pages/LicenseReviewPage.tsx | 60 ++ .../pages/MedicalVerificationPage.tsx | 367 ++++++++- .../app/features/question/api/question-api.ts | 17 + .../features/question/pages/QuestionPage.tsx | 143 +++- .../app/features/question/types/question.ts | 18 + .../src/app/features/result/api/result-api.ts | 58 ++ .../result/components/RecordResultModal.tsx | 71 +- .../features/result/pages/ExamAppealsPage.tsx | 203 +++++ .../app/features/result/pages/ResultPage.tsx | 233 +++++- .../src/app/features/result/types/result.ts | 58 +- .../pages/SeafarerRegistryPage.tsx | 378 ++++++++- .../pages/VesselRegistrationQueuePage.tsx | 359 +++++++- .../features/waiver/pages/WaiverQueuePage.tsx | 21 - .../waiver/pages/WaiverReviewPage.tsx | 21 - apps/backoffice/src/app/i18n/locales/am.ts | 125 ++- apps/backoffice/src/app/i18n/locales/en.ts | 128 ++- apps/backoffice/src/app/layouts/nav-config.ts | 18 +- apps/backoffice/src/app/router/index.tsx | 22 +- .../certificates/pages/CertificatesPage.tsx | 282 ++++++- .../certificates/pages/CoCApplicationPage.tsx | 19 +- .../app/features/exams/pages/ExamsPage.tsx | 406 ++++++++++ .../licensing/components/LicenseCatalogue.tsx | 7 + .../pages/LicenseApplicationPage.tsx | 5 +- .../components/RequireOperations.tsx | 65 +- .../components/OperationsFormContent.tsx | 7 +- .../seafarer/pages/MySeaRecordsPage.tsx | 765 ++++++++++++++++++ .../seafarer/pages/SeafarerProfilePage.tsx | 701 ---------------- .../pages/SeafarerRegistrationPage.tsx | 743 ++++++----------- .../seafarer/pages/SeafarerRegistryPage.tsx | 379 --------- .../verify/pages/VerifyCertificatePage.tsx | 164 ++++ .../pages/VesselOwnerDashboardPage.tsx | 21 - .../pages/VesselOwnerLoginPage.tsx | 128 --- .../pages/VesselOwnerRegisterPage.tsx | 199 ----- .../pages/VesselRegistrationDashboardPage.tsx | 174 ---- .../VesselRegistrationApplicationPage.tsx | 678 ---------------- .../pages/VesselRegistrationPage.tsx | 689 +++++++++------- .../app/features/waiver/pages/WaiverPage.tsx | 221 ++++- apps/portal/src/app/i18n/locales/am.ts | 3 + apps/portal/src/app/i18n/locales/en.ts | 3 + apps/portal/src/app/layouts/PortalLayout.tsx | 14 +- .../src/app/layouts/VesselOwnerLayout.tsx | 80 -- apps/portal/src/app/router.tsx | 63 +- libs/api/src/index.ts | 3 + .../lib/base-api/base-query-with-reauth.ts | 2 +- libs/api/src/lib/base-api/download.ts | 43 + .../lib/features/licensing/licensing-api.ts | 53 +- .../features/licensing/licensing.helpers.ts | 20 +- .../lib/features/licensing/licensing.types.ts | 9 +- libs/api/src/lib/features/seafarer/index.ts | 2 + .../src/lib/features/seafarer/seafarer-api.ts | 188 +++++ .../lib/features/seafarer/seafarer.types.ts | 78 ++ libs/api/src/lib/features/vessel/index.ts | 2 + .../api/src/lib/features/vessel/vessel-api.ts | 84 ++ .../src/lib/features/vessel/vessel.types.ts | 51 ++ libs/auth/src/index.ts | 1 + libs/auth/src/lib/pages/LoginPage.tsx | 113 ++- libs/auth/src/lib/pages/SetPasswordPage.tsx | 152 ++++ libs/auth/src/lib/types/auth.types.ts | 10 + 66 files changed, 6468 insertions(+), 3518 deletions(-) delete mode 100644 apps/backoffice/src/app/features/coc-queue/pages/CoCQueuePage.tsx delete mode 100644 apps/backoffice/src/app/features/coc-queue/pages/CoCReviewPage.tsx create mode 100644 apps/backoffice/src/app/features/exam/components/ExamCandidatesPanel.tsx create mode 100644 apps/backoffice/src/app/features/exam/components/ExamIncidentsPanel.tsx create mode 100644 apps/backoffice/src/app/features/license-register/pages/LicenseRegisterPage.tsx create mode 100644 apps/backoffice/src/app/features/result/pages/ExamAppealsPage.tsx delete mode 100644 apps/backoffice/src/app/features/waiver/pages/WaiverQueuePage.tsx delete mode 100644 apps/backoffice/src/app/features/waiver/pages/WaiverReviewPage.tsx create mode 100644 apps/portal/src/app/features/exams/pages/ExamsPage.tsx create mode 100644 apps/portal/src/app/features/seafarer/pages/MySeaRecordsPage.tsx delete mode 100644 apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx delete mode 100644 apps/portal/src/app/features/seafarer/pages/SeafarerRegistryPage.tsx create mode 100644 apps/portal/src/app/features/verify/pages/VerifyCertificatePage.tsx delete mode 100644 apps/portal/src/app/features/vessel-owner/pages/VesselOwnerDashboardPage.tsx delete mode 100644 apps/portal/src/app/features/vessel-owner/pages/VesselOwnerLoginPage.tsx delete mode 100644 apps/portal/src/app/features/vessel-owner/pages/VesselOwnerRegisterPage.tsx delete mode 100644 apps/portal/src/app/features/vessel-registration-dashboard/pages/VesselRegistrationDashboardPage.tsx delete mode 100644 apps/portal/src/app/features/vessel-registration/pages/VesselRegistrationApplicationPage.tsx delete mode 100644 apps/portal/src/app/layouts/VesselOwnerLayout.tsx create mode 100644 libs/api/src/lib/base-api/download.ts create mode 100644 libs/api/src/lib/features/seafarer/index.ts create mode 100644 libs/api/src/lib/features/seafarer/seafarer-api.ts create mode 100644 libs/api/src/lib/features/seafarer/seafarer.types.ts create mode 100644 libs/api/src/lib/features/vessel/index.ts create mode 100644 libs/api/src/lib/features/vessel/vessel-api.ts create mode 100644 libs/api/src/lib/features/vessel/vessel.types.ts create mode 100644 libs/auth/src/lib/pages/SetPasswordPage.tsx diff --git a/apps/backoffice/src/app/features/coc-queue/pages/CoCQueuePage.tsx b/apps/backoffice/src/app/features/coc-queue/pages/CoCQueuePage.tsx deleted file mode 100644 index 428120fcc..000000000 --- a/apps/backoffice/src/app/features/coc-queue/pages/CoCQueuePage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Container } from '@mantine/core'; -import { FeatureUnavailable } from '@ema-platform/ui'; - -/** - * Placeholder until this feature has a backend. - * - * This page previously rendered hardcoded sample records, which were - * indistinguishable from real ones. - */ -export function CoCQueuePage() { - return ( - - - - ); -} - -export default CoCQueuePage; diff --git a/apps/backoffice/src/app/features/coc-queue/pages/CoCReviewPage.tsx b/apps/backoffice/src/app/features/coc-queue/pages/CoCReviewPage.tsx deleted file mode 100644 index 79d23868d..000000000 --- a/apps/backoffice/src/app/features/coc-queue/pages/CoCReviewPage.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Container } from '@mantine/core'; -import { FeatureUnavailable } from '@ema-platform/ui'; - -/** - * Placeholder until this feature has a backend. - * - * This page previously rendered hardcoded sample records, which were - * indistinguishable from real ones. - */ -export function CoCReviewPage() { - return ( - - - - ); -} - -export default CoCReviewPage; diff --git a/apps/backoffice/src/app/features/exam/api/exam-api.ts b/apps/backoffice/src/app/features/exam/api/exam-api.ts index a99d06399..83dbd473b 100644 --- a/apps/backoffice/src/app/features/exam/api/exam-api.ts +++ b/apps/backoffice/src/app/features/exam/api/exam-api.ts @@ -5,6 +5,12 @@ import type { CreateExamPayload, UpdateExamPayload, AssignQuestionsPayload, + RandomQuestionsPayload, + ExamRegistration, + RecordAttendancePayload, + ExamIncident, + CreateIncidentPayload, + ResolveIncidentPayload, } from '../types/exam'; const examApi = baseApi.injectEndpoints({ @@ -41,6 +47,49 @@ const examApi = baseApi.injectEndpoints({ }), invalidatesTags: ['Api'], }), + /** Server-side draw from the approved bank (US-EXAM-005). */ + selectRandomQuestions: builder.mutation({ + query: ({ examId, count }) => ({ + url: `/exams/${examId}/questions/random`, + method: 'POST', + body: { count }, + }), + invalidatesTags: ['Api'], + }), + // --- Candidates and attendance (US-EXAM-007/009) --------------------- + getExamRegistrations: builder.query({ + query: (examId) => `/exams/${examId}/registrations`, + providesTags: ['Api'], + }), + recordAttendance: builder.mutation({ + query: ({ registrationId, ...body }) => ({ + url: `/exams/registrations/${registrationId}/attendance`, + method: 'POST', + body, + }), + invalidatesTags: ['Api'], + }), + // --- Session incidents (US-EXAM-010) --------------------------------- + getExamIncidents: builder.query({ + query: (examId) => `/exams/${examId}/incidents`, + providesTags: ['Api'], + }), + recordIncident: builder.mutation({ + query: ({ examId, ...body }) => ({ + url: `/exams/${examId}/incidents`, + method: 'POST', + body, + }), + invalidatesTags: ['Api'], + }), + resolveIncident: builder.mutation({ + query: ({ incidentId, ...body }) => ({ + url: `/exams/incidents/${incidentId}/resolve`, + method: 'POST', + body, + }), + invalidatesTags: ['Api'], + }), }), overrideExisting: false, }); @@ -52,4 +101,10 @@ export const { useUpdateExamMutation, useDeleteExamMutation, useAssignQuestionsMutation, + useSelectRandomQuestionsMutation, + useGetExamRegistrationsQuery, + useRecordAttendanceMutation, + useGetExamIncidentsQuery, + useRecordIncidentMutation, + useResolveIncidentMutation, } = examApi; diff --git a/apps/backoffice/src/app/features/exam/components/ExamCandidatesPanel.tsx b/apps/backoffice/src/app/features/exam/components/ExamCandidatesPanel.tsx new file mode 100644 index 000000000..3660b77c9 --- /dev/null +++ b/apps/backoffice/src/app/features/exam/components/ExamCandidatesPanel.tsx @@ -0,0 +1,215 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Alert, + Badge, + Button, + Group, + Modal, + Paper, + Select, + Stack, + Table, + Text, + Textarea, + Title, +} from '@mantine/core'; +import { IconInfoCircle, IconUserCheck } from '@tabler/icons-react'; +import { notify } from '@ema-platform/ui'; +import { extractErrorMessage } from '@ema-platform/api'; +import { + useGetExamRegistrationsQuery, + useRecordAttendanceMutation, +} from '../api/exam-api'; +import type { AttendanceStatus, ExamRegistration } from '../types/exam'; + +const ATTENDANCE_COLOR: Record = { + REGISTERED: 'gray', + PRESENT: 'teal', + LATE: 'yellow', + ABSENT: 'red', + WITHDRAWN: 'orange', + DISQUALIFIED: 'red', +}; + +/** Rulings that end the sitting, and so must be explained (US-EXAM-009). */ +const NEEDS_REMARK: AttendanceStatus[] = ['WITHDRAWN', 'DISQUALIFIED']; + +const OPTIONS: AttendanceStatus[] = [ + 'PRESENT', + 'LATE', + 'ABSENT', + 'WITHDRAWN', + 'DISQUALIFIED', +]; + +/** + * The invigilator's register for one session (US-EXAM-009). + * + * Marking a paper later depends on what is recorded here: an absent, + * withdrawn or disqualified candidate has no result to enter. + */ +export function ExamCandidatesPanel({ examId }: { examId: string }) { + const { t } = useTranslation(); + const { data: registrations, isError } = useGetExamRegistrationsQuery(examId); + const [recordAttendance, { isLoading }] = useRecordAttendanceMutation(); + const [target, setTarget] = useState(null); + const [status, setStatus] = useState('PRESENT'); + const [remark, setRemark] = useState(''); + + const candidateName = (registration: ExamRegistration) => + [ + registration.profile?.firstName, + registration.profile?.middleName, + registration.profile?.lastName, + ] + .filter(Boolean) + .join(' ') || registration.profileId.slice(0, 8); + + const save = async () => { + if (!target) return; + if (NEEDS_REMARK.includes(status) && !remark.trim()) { + notify.error(t('exam.candidates.remarkRequired')); + return; + } + try { + await recordAttendance({ + registrationId: target.id, + status, + remark: remark.trim() || undefined, + }).unwrap(); + notify.success(t('exam.candidates.recorded')); + setTarget(null); + setRemark(''); + } catch (error) { + notify.error(extractErrorMessage(error, t('exam.incidents.error'))); + } + }; + + // Officers without the invigilation permission simply do not see the + // register; the endpoint refuses them and there is nothing to show. + if (isError) return null; + + return ( + + + {t('exam.candidates.section')} + + {(registrations ?? []).length === 0 ? ( + }> + {t('exam.candidates.none')} + + ) : ( + + + + {t('exam.candidates.admission')} + {t('exam.candidates.name')} + {t('exam.candidates.attempt')} + {t('exam.candidates.attendance')} + {t('exam.candidates.remark')} + + + + + {(registrations ?? []).map((registration) => ( + + + + {registration.admissionNumber} + + + + {candidateName(registration)} + + + + {registration.kind === 'RETAKE' + ? t('exam.candidates.retake', { n: registration.attemptNumber }) + : t('exam.candidates.firstSitting')} + + + + + {t(`exam.attendance.${registration.attendanceStatus}`)} + + + + + {registration.attendanceRemark ?? '—'} + + + + + + + ))} + +
+ )} + + setTarget(null)} + title={t('exam.candidates.attendance')} + size="md" + radius="lg" + > + + + {target ? candidateName(target) : ''} · {target?.admissionNumber} + +