From 6b5b99f42d3877107fe9ff10d1d1e607004665db Mon Sep 17 00:00:00 2001 From: Fistum Date: Fri, 21 Aug 2026 11:16:22 +0000 Subject: [PATCH] ui chnages --- .../components/ExamIncidentsPanel/columns.tsx | 21 +- .../features/exam/pages/ExamDetailPage.tsx | 26 +- .../features/exam/pages/ExamPage/columns.tsx | 25 +- .../item/components/ItemTable/columns.tsx | 13 +- .../pages/LicenseRegisterPage/columns.tsx | 25 +- .../pages/MedicalVerificationPage/columns.tsx | 19 +- .../question/pages/QuestionPage/index.tsx | 19 +- .../result/pages/ResultPage/columns.tsx | 22 +- .../result/pages/ResultPage/index.tsx | 51 +- .../pages/SeafarerDocumentQueuePage.tsx | 50 +- .../pages/SeafarerRegistrationQueuePage.tsx | 61 +- .../pages/SeafarerRegistrationReviewPage.tsx | 11 +- .../pages/SeafarerRegistryPage.tsx | 13 +- .../VesselRegistrationQueuePage/columns.tsx | 21 +- apps/e2e/src/seafarer-registration.spec.ts | 10 +- .../pages/BasicSafetyTrainingPage.tsx | 28 +- .../components/steps.tsx | 39 +- .../pages/SeafarerRegistrationPage.tsx | 51 +- .../seafarer/pages/SeaRecords/columns.tsx | 24 +- .../seafarer/pages/SeafarerProfilePage.tsx | 20 +- .../seafarer/pages/SeafarerRegistryPage.tsx | 37 +- .../pages/OwnershipTransferPage.tsx | 20 +- .../pages/VesselRegistrationPage.tsx | 25 +- .../pages/VesselTransferPage.tsx | 19 +- .../seafarer-registration.constants.ts | 26 +- libs/shared/src/lib/theme/base-theme.ts | 35 + libs/ui/src/index.ts | 1 + libs/ui/src/lib/data/AdvancedTable.tsx | 50 +- libs/ui/src/lib/feedback/StatusBadge.tsx | 30 + libs/ui/src/lib/layout/AppSidebar.tsx | 9 +- pnpm-lock.yaml | 2014 ++++++----------- 31 files changed, 1173 insertions(+), 1642 deletions(-) create mode 100644 libs/ui/src/lib/feedback/StatusBadge.tsx diff --git a/apps/backoffice/src/app/features/exam/components/ExamIncidentsPanel/columns.tsx b/apps/backoffice/src/app/features/exam/components/ExamIncidentsPanel/columns.tsx index 5d5fab6fd..16dfffc1f 100644 --- a/apps/backoffice/src/app/features/exam/components/ExamIncidentsPanel/columns.tsx +++ b/apps/backoffice/src/app/features/exam/components/ExamIncidentsPanel/columns.tsx @@ -1,15 +1,17 @@ +import { type StatusTone } from '@ema-platform/shared'; import { Badge, Button, Text } from '@mantine/core'; import { IconAlertTriangle } from '@tabler/icons-react'; import type { TFunction } from 'i18next'; import type { AdvancedColumn } from '@ema-platform/ui'; +import { StatusBadge } from '@ema-platform/ui'; import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth'; import type { ExamIncident, ExamIncidentStatus } from '../../types/exam'; -const STATUS_COLOR: Record = { - OPEN: 'red', - UNDER_REVIEW: 'yellow', - RESOLVED: 'teal', - DISMISSED: 'gray', +const STATUS_TONE: Record = { + OPEN: 'danger', + UNDER_REVIEW: 'warning', + RESOLVED: 'success', + DISMISSED: 'neutral', }; export function examIncidentColumns( @@ -56,13 +58,12 @@ export function examIncidentColumns( { header: t('exam.incidents.status'), cell: ({ row }) => ( - - {t(`exam.incidentStatus.${row.original.status}`)} - + /> ), }, { diff --git a/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx b/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx index 6c09dd93b..dbf134fe3 100644 --- a/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx +++ b/apps/backoffice/src/app/features/exam/pages/ExamDetailPage.tsx @@ -1,3 +1,4 @@ +import { type StatusTone } from '@ema-platform/shared'; import { useState, useEffect, useRef, useMemo } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { @@ -39,7 +40,7 @@ import { IconCheck, IconX, } from '@tabler/icons-react'; -import { ModalFooter, notify, useErrorHandler } from '@ema-platform/ui'; +import { StatusBadge, ModalFooter, notify, useErrorHandler } from '@ema-platform/ui'; import { extractErrorMessage } from '@ema-platform/api'; import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth'; import { @@ -57,13 +58,13 @@ import { ExamIncidentsPanel } from '../components/ExamIncidentsPanel'; import { PageLoader } from '@ema-platform/ui'; import type { ExamStatus, QuestionBrief } from '../types/exam'; -const STATUS_COLOR: Record = { - PENDING: "gray", - ACTIVE: "blue", - COMPLETED: "teal", - CANCELLED: "red", - POSTPONED: "orange", - PUBLISHED: "green", +const STATUS_TONE: Record = { + PENDING: 'neutral', + ACTIVE: 'info', + COMPLETED: 'success', + CANCELLED: 'danger', + POSTPONED: 'pending', + PUBLISHED: 'success', }; const FORM_LABEL: Record = { ESSAY: "Essay", CHOICE: "Choice" }; @@ -313,14 +314,13 @@ export function ExamDetailPage() { {/* Status badge */} - - {t(`exam.status.${exam.status}`)} - + /> {/* Exam Info */} diff --git a/apps/backoffice/src/app/features/exam/pages/ExamPage/columns.tsx b/apps/backoffice/src/app/features/exam/pages/ExamPage/columns.tsx index a5ec1970f..4fddd6d14 100644 --- a/apps/backoffice/src/app/features/exam/pages/ExamPage/columns.tsx +++ b/apps/backoffice/src/app/features/exam/pages/ExamPage/columns.tsx @@ -1,15 +1,17 @@ +import { type StatusTone } from '@ema-platform/shared'; import { Badge, Text } from "@mantine/core"; import type { TFunction } from "i18next"; import type { AdvancedColumn } from "@ema-platform/ui"; +import { StatusBadge } from '@ema-platform/ui'; import type { Exam } from "../../types/exam"; -const STATUS_COLOR: Record = { - PENDING: "gray", - ACTIVE: "blue", - COMPLETED: "teal", - CANCELLED: "red", - POSTPONED: "orange", - PUBLISHED: "green", +const STATUS_TONE: Record = { + PENDING: 'neutral', + ACTIVE: 'info', + COMPLETED: 'success', + CANCELLED: 'danger', + POSTPONED: 'pending', + PUBLISHED: 'success', }; export function examColumns( @@ -72,9 +74,12 @@ export function examColumns( { header: t("exam.columns.status"), cell: ({ row }) => ( - - {t(`exam.status.${row.original.status}`)} - + ), }, ]; diff --git a/apps/backoffice/src/app/features/item/components/ItemTable/columns.tsx b/apps/backoffice/src/app/features/item/components/ItemTable/columns.tsx index 7a0f9e447..baf895ec2 100644 --- a/apps/backoffice/src/app/features/item/components/ItemTable/columns.tsx +++ b/apps/backoffice/src/app/features/item/components/ItemTable/columns.tsx @@ -1,11 +1,12 @@ -import { Badge } from '@mantine/core'; +import { type StatusTone } from '@ema-platform/shared'; import type { AdvancedColumn } from '@ema-platform/ui'; +import { StatusBadge } from '@ema-platform/ui'; import type { Item } from '../../api/item-api'; -const STATUS_COLORS: Record = { - DRAFT: 'gray', - ACTIVE: 'green', - ARCHIVED: 'orange', +const STATUS_TONES: Record = { + DRAFT: 'neutral', + ACTIVE: 'success', + ARCHIVED: 'pending', }; export function itemColumns(showDate: (date: string) => string): AdvancedColumn[] { @@ -14,7 +15,7 @@ export function itemColumns(showDate: (date: string) => string): AdvancedColumn< { header: 'Status', cell: ({ row }) => ( - {row.original.status} + ), }, { diff --git a/apps/backoffice/src/app/features/license-register/pages/LicenseRegisterPage/columns.tsx b/apps/backoffice/src/app/features/license-register/pages/LicenseRegisterPage/columns.tsx index b168c8908..965c9b4d5 100644 --- a/apps/backoffice/src/app/features/license-register/pages/LicenseRegisterPage/columns.tsx +++ b/apps/backoffice/src/app/features/license-register/pages/LicenseRegisterPage/columns.tsx @@ -1,15 +1,17 @@ -import { Badge, Button, Text, Tooltip } from '@mantine/core'; +import { type StatusTone } from '@ema-platform/shared'; +import { Button, Text, Tooltip } from '@mantine/core'; import { IconShieldCog } from '@tabler/icons-react'; import type { AdvancedColumn } from '@ema-platform/ui'; +import { StatusBadge } from '@ema-platform/ui'; import type { Bilingual, IssuedLicense } from '@ema-platform/api'; import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth'; -const LICENSE_STATUS_COLORS: Record = { - ACTIVE: 'green', - EXPIRED: 'yellow', - SUSPENDED: 'orange', - CANCELLED: 'red', - SUPERSEDED: 'gray', +const LICENSE_STATUS_TONES: Record = { + ACTIVE: 'success', + EXPIRED: 'warning', + SUSPENDED: 'pending', + CANCELLED: 'danger', + SUPERSEDED: 'neutral', }; export type LifecycleAction = 'suspend' | 'revoke' | 'reinstate'; @@ -70,13 +72,12 @@ export function licenseRegisterColumns( { header: 'Status', cell: ({ row }) => ( - - {row.original.status} - + /> ), }, { diff --git a/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/columns.tsx b/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/columns.tsx index ce6450686..77dd56496 100644 --- a/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/columns.tsx +++ b/apps/backoffice/src/app/features/medical-verification/pages/MedicalVerificationPage/columns.tsx @@ -1,6 +1,8 @@ +import { type StatusTone } from '@ema-platform/shared'; import { Badge, Text } from '@mantine/core'; import type { TFunction } from 'i18next'; import type { AdvancedColumn } from '@ema-platform/ui'; +import { StatusBadge } from '@ema-platform/ui'; import { seaServiceDays, type MedicalCertificate, @@ -18,10 +20,10 @@ export function ownerName(profile?: SeafarerProfileSummary): string { ); } -const STATUS_COLOR: Record = { - SUBMITTED: 'yellow', - VERIFIED: 'teal', - REJECTED: 'red', +const STATUS_TONE: Record = { + SUBMITTED: 'warning', + VERIFIED: 'success', + REJECTED: 'danger', }; /** Only meaningful now the queue can show ruled records too. */ @@ -33,9 +35,12 @@ function statusColumn( label: t('recordVerification.columns.status', 'Status'), accessorKey: 'status', cell: ({ row }) => ( - - {t(`recordVerification.status.${row.original.status}`, row.original.status)} - + ), }; } diff --git a/apps/backoffice/src/app/features/question/pages/QuestionPage/index.tsx b/apps/backoffice/src/app/features/question/pages/QuestionPage/index.tsx index e5026c659..4f72d2228 100644 --- a/apps/backoffice/src/app/features/question/pages/QuestionPage/index.tsx +++ b/apps/backoffice/src/app/features/question/pages/QuestionPage/index.tsx @@ -8,7 +8,6 @@ import { Modal, Text, TextInput, - Card, Alert, Select, NumberInput, @@ -238,9 +237,12 @@ export function QuestionPage() { /> )} - - - {t('question.pool')} + - - - = { - PASSED: 'teal', - FAILED: 'red', +export const STATUS_TONE: Record = { + PASSED: 'success', + FAILED: 'danger', }; /** Where a mark sits in quality control (US-EXAM-011 → 014). */ @@ -46,20 +48,22 @@ export function resultColumns( { header: t('result.columns.status'), cell: ({ row }) => ( - } - > - {t(`result.status.${row.original.status}`)} - + /> ), }, { diff --git a/apps/backoffice/src/app/features/result/pages/ResultPage/index.tsx b/apps/backoffice/src/app/features/result/pages/ResultPage/index.tsx index 583ea57d0..015ef7e68 100644 --- a/apps/backoffice/src/app/features/result/pages/ResultPage/index.tsx +++ b/apps/backoffice/src/app/features/result/pages/ResultPage/index.tsx @@ -9,7 +9,6 @@ import { Modal, Text, Paper, - Card, Loader, Center, Alert, @@ -34,7 +33,7 @@ import { IconSearch, IconSend, } from '@tabler/icons-react'; -import { notify, BilingualInput, useErrorHandler, AdvancedTable, useServerTable, ModalFooter } from '@ema-platform/ui'; +import { notify, BilingualInput, useErrorHandler, AdvancedTable, StatusBadge, useServerTable, ModalFooter } from '@ema-platform/ui'; import { useDateDisplayer } from '@ema-platform/shared'; import type { BilingualValue } from '@ema-platform/ui'; import { extractErrorMessage, useLocalized } from '@ema-platform/api'; @@ -53,7 +52,7 @@ import { useGetExamsQuery } from '../../../exam/api/exam-api'; import { RecordResultModal } from '../../components/RecordResultModal'; import type { Result, ResultBreakdown } from '../../types/result'; import type { Exam } from '../../../exam/types/exam'; -import { resultColumns, STATUS_COLOR, REVIEW_COLOR } from './columns'; +import { resultColumns, STATUS_TONE, REVIEW_COLOR } from './columns'; import { resultActionsColumn, type QcAction } from './actions'; function ResultStat({ @@ -327,10 +326,13 @@ export function ResultPage() { - - - {t('result.section')} - + } @@ -348,23 +350,17 @@ export function ResultPage() { style={{ width: 280 }} clearable /> - - - - - + + } + itemCount={page.itemCount} + pageIndex={page.pageIndex} + onPageChange={setPageIndex} + pageSize={pageSize} + onPageSizeChange={setPageSize} + refresh={refetch} + isLoading={isFetching} + emptyText={t('result.noItems')} + /> - - {t(`result.status.${detailResult.status}`)} - + {t(`result.review.${detailResult.reviewStatus}`)} diff --git a/apps/backoffice/src/app/features/seafarer-document-review/pages/SeafarerDocumentQueuePage.tsx b/apps/backoffice/src/app/features/seafarer-document-review/pages/SeafarerDocumentQueuePage.tsx index 961545a97..4c9e3c0aa 100644 --- a/apps/backoffice/src/app/features/seafarer-document-review/pages/SeafarerDocumentQueuePage.tsx +++ b/apps/backoffice/src/app/features/seafarer-document-review/pages/SeafarerDocumentQueuePage.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Badge, Container, Group, Select, Text, TextInput, Title } from '@mantine/core'; +import {Badge, Container, Select, Text, TextInput, Title} from '@mantine/core'; import { IconSearch } from '@tabler/icons-react'; import { useDebouncedValue } from '@mantine/hooks'; import { @@ -113,33 +113,35 @@ export function SeafarerDocumentQueuePage({ kind }: { kind: SeafarerDocumentKind Requests released by an approved seafarer registration: confirm payment, schedule the collection date, then issue. - - } - value={search} - onChange={(e) => { - setSearch(e.currentTarget.value); - setPage(0); - }} - w={280} - /> - { + setStatus(v as SeafarerDocumentStatus | null); + setPage(0); + }} + clearable + w={200} + /> + + } itemCount={data?.total ?? 0} pageIndex={page} onPageChange={setPage} diff --git a/apps/backoffice/src/app/features/seafarer-registration-review/pages/SeafarerRegistrationQueuePage.tsx b/apps/backoffice/src/app/features/seafarer-registration-review/pages/SeafarerRegistrationQueuePage.tsx index 26858983f..9e1bff58f 100644 --- a/apps/backoffice/src/app/features/seafarer-registration-review/pages/SeafarerRegistrationQueuePage.tsx +++ b/apps/backoffice/src/app/features/seafarer-registration-review/pages/SeafarerRegistrationQueuePage.tsx @@ -1,17 +1,17 @@ import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Badge, Container, Group, Select, Text, TextInput, Title } from '@mantine/core'; +import {Container, Select, Text, TextInput, Title} from '@mantine/core'; import { IconSearch } from '@tabler/icons-react'; import { useDebouncedValue } from '@mantine/hooks'; import { - SEAFARER_REGISTRATION_STATUS_COLORS, + SEAFARER_REGISTRATION_STATUS_TONES, SEAFARER_REGISTRATION_STATUS_LABELS, displaySeafarerAnswer, useListSeafarerRegistrationsQuery, type SeafarerRegistration, type SeafarerRegistrationStatus, } from '@ema-platform/api'; -import { AdvancedTable, type AdvancedColumn } from '@ema-platform/ui'; +import { AdvancedTable, StatusBadge, type AdvancedColumn } from '@ema-platform/ui'; import { useDateDisplayer } from '@ema-platform/shared'; const PAGE_SIZE = 10; @@ -82,9 +82,10 @@ export function SeafarerRegistrationQueuePage() { header: 'Status', accessorKey: 'status', cell: ({ row }) => ( - - {SEAFARER_REGISTRATION_STATUS_LABELS[row.original.status]} - + ), }, ], @@ -100,33 +101,35 @@ export function SeafarerRegistrationQueuePage() { Registrations awaiting review. Approval numbers the seafarer and opens their Seaman Book and BTC applications. - - } - value={search} - onChange={(e) => { - setSearch(e.currentTarget.value); - setPage(0); - }} - w={280} - /> - { + setStatus(v as SeafarerRegistrationStatus | null); + setPage(0); + }} + clearable + w={200} + /> + + } itemCount={data?.total ?? 0} pageIndex={page} onPageChange={setPage} diff --git a/apps/backoffice/src/app/features/seafarer-registration-review/pages/SeafarerRegistrationReviewPage.tsx b/apps/backoffice/src/app/features/seafarer-registration-review/pages/SeafarerRegistrationReviewPage.tsx index d31183eae..bc1433241 100644 --- a/apps/backoffice/src/app/features/seafarer-registration-review/pages/SeafarerRegistrationReviewPage.tsx +++ b/apps/backoffice/src/app/features/seafarer-registration-review/pages/SeafarerRegistrationReviewPage.tsx @@ -22,7 +22,7 @@ import { SEAFARER_REGISTRATION_DOCUMENTS, SEAFARER_REGISTRATION_FIELD_LABELS, SEAFARER_REGISTRATION_SECTIONS, - SEAFARER_REGISTRATION_STATUS_COLORS, + SEAFARER_REGISTRATION_STATUS_TONES, SEAFARER_REGISTRATION_STATUS_LABELS, displaySeafarerAnswer, extractErrorMessage, @@ -31,7 +31,7 @@ import { useRejectSeafarerRegistrationMutation, useRequestSeafarerRegistrationChangesMutation, } from '@ema-platform/api'; -import { notify } from '@ema-platform/ui'; +import { notify, StatusBadge } from '@ema-platform/ui'; import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth'; import { applicantName } from './SeafarerRegistrationQueuePage'; @@ -116,9 +116,10 @@ export function SeafarerRegistrationReviewPage() { {registration.registrationNumber} - - {SEAFARER_REGISTRATION_STATUS_LABELS[registration.status]} - + {registration.seafarerNumber && ( }> {registration.seafarerNumber} diff --git a/apps/backoffice/src/app/features/seafarer-registry/pages/SeafarerRegistryPage.tsx b/apps/backoffice/src/app/features/seafarer-registry/pages/SeafarerRegistryPage.tsx index 5efcd6776..5ed329330 100644 --- a/apps/backoffice/src/app/features/seafarer-registry/pages/SeafarerRegistryPage.tsx +++ b/apps/backoffice/src/app/features/seafarer-registry/pages/SeafarerRegistryPage.tsx @@ -1,3 +1,5 @@ +import { StatusBadge } from '@ema-platform/ui'; +import { type StatusTone } from '@ema-platform/shared'; import { useState } from 'react'; import { useApiQuery } from '@ema-platform/api'; import { @@ -59,7 +61,7 @@ const STATUS_OPTIONS = ['All', 'Active', 'Inactive', 'Suspended']; const MEDICAL_OPTIONS = ['All', 'Valid', 'Expiring', 'Expired']; const MEDICAL_COLOR: Record = { Valid: 'teal', Expiring: 'orange', Expired: 'red' }; -const STATUS_COLOR: Record = { Active: 'teal', Inactive: 'gray', Suspended: 'red' }; +const STATUS_TONE: Record = { Active: 'success', Inactive: 'neutral', Suspended: 'danger' }; // --------------------------------------------------------------------------- // Detail modal @@ -83,7 +85,7 @@ function DetailModal({ sf, opened, onClose }: { sf: Seafarer | null; opened: boo Nationality{sf.nationality} Date of Birth{sf.dob} Rank{sf.rank} - Status{sf.status} + Status @@ -264,7 +266,12 @@ export function SeafarerRegistryPage() { : } - {sf.status} + setSelected(sf)}> diff --git a/apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationQueuePage/columns.tsx b/apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationQueuePage/columns.tsx index b912d4677..5c11db5ce 100644 --- a/apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationQueuePage/columns.tsx +++ b/apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationQueuePage/columns.tsx @@ -1,13 +1,15 @@ -import { Badge, Button, Text, Tooltip } from '@mantine/core'; +import { type StatusTone } from '@ema-platform/shared'; +import { Button, Text, Tooltip } from '@mantine/core'; import { IconShieldCog } from '@tabler/icons-react'; import type { AdvancedColumn } from '@ema-platform/ui'; +import { StatusBadge } from '@ema-platform/ui'; import type { Vessel } from '@ema-platform/api'; import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth'; -const VESSEL_STATUS_COLORS: Record = { - REGISTERED: 'green', - SUSPENDED: 'orange', - DEREGISTERED: 'gray', +const VESSEL_STATUS_TONES: Record = { + REGISTERED: 'success', + SUSPENDED: 'pending', + DEREGISTERED: 'neutral', }; export const CATEGORY_LABELS: Record = { @@ -63,13 +65,12 @@ export function vesselRegistrationQueueColumns( { header: 'Status', cell: ({ row }) => ( - - {row.original.status} - + /> ), }, { diff --git a/apps/e2e/src/seafarer-registration.spec.ts b/apps/e2e/src/seafarer-registration.spec.ts index 6fab32b35..b98537e6e 100644 --- a/apps/e2e/src/seafarer-registration.spec.ts +++ b/apps/e2e/src/seafarer-registration.spec.ts @@ -347,7 +347,7 @@ test.describe('seafarer registration', () => { await expect(page.getByLabel('National ID (Fayda) Number')).toHaveValue('FYD1234567890'); await page.getByRole('button', { name: /^continue$/i }).click(); - // Step 2 — Applicant Details. + // Step 2 — Details: address and physical characteristics. await page.getByLabel('Place of Birth').fill('Addis Ababa'); await pick(page, 'Department', /deck/i); await pick(page, 'City', /addis ababa/i); @@ -356,15 +356,15 @@ test.describe('seafarer registration', () => { await pick(page, 'Eye Colour', /brown/i); await page.getByLabel('Height (cm)').fill('172'); await page.getByLabel('Weight (kg)').fill('68'); - await page.getByLabel('Certificate Number').fill('MED-2026-001'); - await page.getByLabel('Issuing Clinic or Practitioner').fill('Addis Marine Clinic'); - await pickDate(page, 'Issue Date', '2026-01-15'); await page.getByRole('button', { name: /^continue$/i }).click(); - // Step 3 — Emergency Contact. + // Step 3 — Contact & Medical. await page.getByLabel('Full Name').fill('Almaz Tesfaye'); await page.getByLabel('Relationship').fill('Sister'); await page.getByLabel('Phone Number').fill('+251911222333'); + await page.getByLabel('Certificate Number').fill('MED-2026-001'); + await page.getByLabel('Issuing Clinic or Practitioner').fill('Addis Marine Clinic'); + await pickDate(page, 'Issue Date', '2026-01-15'); await page.getByRole('button', { name: /^continue$/i }).click(); // Step 4 — Documents: all four required slots show as uploaded. diff --git a/apps/portal/src/app/features/basic-safety-training/pages/BasicSafetyTrainingPage.tsx b/apps/portal/src/app/features/basic-safety-training/pages/BasicSafetyTrainingPage.tsx index a59a7b514..4ac813385 100644 --- a/apps/portal/src/app/features/basic-safety-training/pages/BasicSafetyTrainingPage.tsx +++ b/apps/portal/src/app/features/basic-safety-training/pages/BasicSafetyTrainingPage.tsx @@ -1,3 +1,4 @@ +import { type StatusTone, STATUS_TONE_COLOR } from '@ema-platform/shared'; import { useRef, useState } from 'react'; import { useApiQuery } from '@ema-platform/api'; import { @@ -30,7 +31,7 @@ import { IconTrash, IconUpload, } from '@tabler/icons-react'; -import { notify } from '@ema-platform/ui'; +import { StatusBadge, notify } from '@ema-platform/ui'; import { useTranslation } from 'react-i18next'; // --------------------------------------------------------------------------- @@ -45,11 +46,11 @@ interface BSTRecord { status: 'Valid' | 'Expiring' | 'Expired' | 'Pending Verification'; } -const STATUS_COLOR: Record = { - Valid: 'teal', - Expiring: 'orange', - Expired: 'red', - 'Pending Verification': 'yellow', +const STATUS_TONE: Record = { + Valid: 'success', + Expiring: 'pending', + Expired: 'danger', + 'Pending Verification': 'warning', }; /** Progress across the five STCW A-VI/1 modules, as `/bst/my` reports it. */ @@ -289,14 +290,13 @@ export function BasicSafetyTrainingPage() { {record && ( - } - > - {record.status} - + /> )} @@ -321,7 +321,7 @@ export function BasicSafetyTrainingPage() { - +
@@ -329,9 +329,7 @@ export function BasicSafetyTrainingPage() { Combined certificate — all 5 STCW components
- - {record.status} - +
diff --git a/apps/portal/src/app/features/seafarer-registration/components/steps.tsx b/apps/portal/src/app/features/seafarer-registration/components/steps.tsx index c15fbfc10..d75e7f585 100644 --- a/apps/portal/src/app/features/seafarer-registration/components/steps.tsx +++ b/apps/portal/src/app/features/seafarer-registration/components/steps.tsx @@ -72,7 +72,7 @@ export function IdentityDetailsStep( ); } -/** Step 2 — Identity, Address, Physical Characteristics, Medical Certificate. */ +/** Step 2 — Identity, Address and Physical Characteristics. */ export function ApplicantDetailsStep(p: StepProps) { return ( @@ -138,6 +138,29 @@ export function ApplicantDetailsStep(p: StepProps) { /> + + ); +} + +/** + * Step 4 — Emergency Contact and Medical Certificate. + * + * The medical certificate used to sit at the bottom of Applicant Details, + * which made that step 17 fields across four sections while this one held + * three. Both are short, unrelated-to-identity, and copied off a document in + * hand rather than recalled — so they pair here and the wizard's longest step + * drops by a third. + */ +export function EmergencyContactStep(p: StepProps) { + return ( + + + + + + + + ); } - -/** Step 3 — Emergency Contact. */ -export function EmergencyContactStep(p: StepProps) { - return ( - - - - - - - - - ); -} diff --git a/apps/portal/src/app/features/seafarer-registration/pages/SeafarerRegistrationPage.tsx b/apps/portal/src/app/features/seafarer-registration/pages/SeafarerRegistrationPage.tsx index f4ef3e17a..bcef33bf2 100644 --- a/apps/portal/src/app/features/seafarer-registration/pages/SeafarerRegistrationPage.tsx +++ b/apps/portal/src/app/features/seafarer-registration/pages/SeafarerRegistrationPage.tsx @@ -1,7 +1,6 @@ import { useEffect, useRef, useState } from 'react'; import { Alert, - Badge, Button, Center, Container, @@ -20,7 +19,7 @@ import { notifications } from '@mantine/notifications'; import { PHYSICAL_BOUNDS, SEAFARER_REGISTRATION_FIELD_LABELS, - SEAFARER_REGISTRATION_STATUS_COLORS, + SEAFARER_REGISTRATION_STATUS_TONES, SEAFARER_REGISTRATION_STATUS_LABELS, extractErrorMessage, extractValidationIssues, @@ -33,7 +32,7 @@ import { type SeafarerRegistration, type ValidationIssue, } from '@ema-platform/api'; -import { splitPersonName } from '@ema-platform/ui'; +import { splitPersonName, StatusBadge } from '@ema-platform/ui'; import { useCurrentProfile, type CurrentProfile } from '@ema-platform/auth'; import { useAppSelector } from '../../../store/hooks'; import { CheckboxField, type AnswerKey } from '../components/fields'; @@ -41,16 +40,19 @@ import { ApplicantDetailsStep, EmergencyContactStep, IdentityDetailsStep } from import { RegistrationDocuments, documentSlots } from '../components/RegistrationDocuments'; import { RegistrationSummary } from '../components/RegistrationSummary'; -const STEPS = ['Identity Details', 'Applicant Details', 'Emergency Contact', 'Documents', 'Review']; +const STEPS = [ + { label: 'Identity', description: 'Who you are' }, + { label: 'Details', description: 'Address & physical' }, + { label: 'Contact & Medical', description: 'Emergency & fitness' }, + { label: 'Documents', description: 'Upload evidence' }, + { label: 'Review', description: 'Check & submit' }, +]; /** Which answers each step must have before "Continue" — mirrors the API's submission check. */ const REQUIRED_BY_STEP: AnswerKey[][] = [ ['firstName', 'lastName', 'gender', 'dateOfBirth', 'maritalStatus', 'nationality', 'nationalIdNumber'], - [ - 'placeOfBirth', 'department', 'locationId', 'hairColor', 'eyeColor', 'heightCm', 'weightKg', - 'medicalCertificateNumber', 'medicalIssuerName', 'medicalIssueDate', - ], - [], + ['placeOfBirth', 'department', 'locationId', 'hairColor', 'eyeColor', 'heightCm', 'weightKg'], + ['medicalCertificateNumber', 'medicalIssuerName', 'medicalIssueDate'], [], ['declarationAccepted'], ]; @@ -123,7 +125,7 @@ export function SeafarerRegistrationPage() { const registration = data?.registration ?? null; const [start] = useStartSeafarerRegistrationMutation(); - const [save] = useSaveSeafarerRegistrationMutation(); + const [save, { isLoading: saving }] = useSaveSeafarerRegistrationMutation(); const [submit, { isLoading: submitting }] = useSubmitSeafarerRegistrationMutation(); const [startError, setStartError] = useState(null); const started = useRef(false); @@ -215,12 +217,16 @@ export function SeafarerRegistrationPage() { } } setErrors(found); - const count = Object.keys(found).length; - if (count) { + const missingKeys = Object.keys(found) as AnswerKey[]; + if (missingKeys.length) { + // Name the fields rather than counting them. "Complete 3 required fields" + // sends the applicant hunting up a step they have already scrolled past; + // the labels are what let them go straight to it. + const names = missingKeys.map((k) => SEAFARER_REGISTRATION_FIELD_LABELS[k]); notifications.show({ color: 'red', - title: 'Incomplete', - message: `Complete ${count} required field${count > 1 ? 's' : ''} to continue.`, + title: missingKeys.length > 1 ? 'Some details are missing' : 'One detail is missing', + message: `${names.join(', ')}.`, }); return false; } @@ -307,9 +313,10 @@ export function SeafarerRegistrationPage() { {registration.registrationNumber} - - {SEAFARER_REGISTRATION_STATUS_LABELS[registration.status]} - + {showSummary && !readOnly && ( @@ -362,8 +369,8 @@ export function SeafarerRegistrationPage() { {!showSummary && ( - {STEPS.map((label) => ( - + {STEPS.map((step) => ( + ))} @@ -408,9 +415,11 @@ export function SeafarerRegistrationPage() { Back {active < STEPS.length - 1 ? ( - + ) : ( - )} diff --git a/apps/portal/src/app/features/seafarer/pages/SeaRecords/columns.tsx b/apps/portal/src/app/features/seafarer/pages/SeaRecords/columns.tsx index b9956e79d..4623d7657 100644 --- a/apps/portal/src/app/features/seafarer/pages/SeaRecords/columns.tsx +++ b/apps/portal/src/app/features/seafarer/pages/SeaRecords/columns.tsx @@ -1,12 +1,14 @@ +import { type StatusTone } from '@ema-platform/shared'; import { Badge, Group, Text, Tooltip } from '@mantine/core'; import type { TFunction } from 'i18next'; import type { AdvancedColumn } from '@ema-platform/ui'; +import { StatusBadge } from '@ema-platform/ui'; import { seaServiceDays, type MedicalCertificate, type SeaServiceRecord } from '@ema-platform/api'; -const RECORD_STATUS_COLORS: Record = { - SUBMITTED: 'blue', - VERIFIED: 'green', - REJECTED: 'red', +const RECORD_STATUS_TONES: Record = { + SUBMITTED: 'info', + VERIFIED: 'success', + REJECTED: 'danger', }; export function fitnessOptions(t: TFunction) { @@ -78,11 +80,12 @@ export function seaServiceColumns( label={row.original.verificationRemark ?? ''} disabled={!row.original.verificationRemark} > - - {t(`seaRecords.columns.recordStatus.${row.original.status}`, { + + /> ), }, @@ -140,11 +143,12 @@ export function medicalColumns( label={row.original.verificationRemark ?? ''} disabled={!row.original.verificationRemark} > - - {t(`seaRecords.columns.recordStatus.${row.original.status}`, { + + /> ), }, diff --git a/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx b/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx index f1e7f00f6..407b77a69 100644 --- a/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx +++ b/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx @@ -1,3 +1,4 @@ +import { type StatusTone } from '@ema-platform/shared'; import { useEffect, useState } from 'react'; import { ActionIcon, @@ -45,7 +46,7 @@ import { IconX, } from '@tabler/icons-react'; import { useNavigate, useParams } from 'react-router-dom'; -import { notify } from '@ema-platform/ui'; +import { StatusBadge, notify } from '@ema-platform/ui'; import type { Seafarer } from './SeafarerRegistryPage'; // --------------------------------------------------------------------------- @@ -170,14 +171,14 @@ async function updateSeafarerStatus(_id: string, _status: string): Promise // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -const STATUS_COLOR: Record = { - Active: 'teal', Pending: 'yellow', Suspended: 'red', - Approved: 'teal', Expired: 'red', Valid: 'teal', - Fit: 'teal', Unfit: 'red', Conditional: 'orange', +const STATUS_TONE: Record = { + Active: 'success', Pending: 'warning', Suspended: 'danger', + Approved: 'success', Expired: 'danger', Valid: 'success', + Fit: 'success', Unfit: 'danger', Conditional: 'pending', }; function Chip({ value }: { value: string }) { - return {value}; + return ; } function InfoField({ label, value }: { label: string; value: string }) { @@ -639,7 +640,12 @@ export function SeafarerProfilePage() { - {profile.status} + diff --git a/apps/portal/src/app/features/seafarer/pages/SeafarerRegistryPage.tsx b/apps/portal/src/app/features/seafarer/pages/SeafarerRegistryPage.tsx index cbbc6e310..e29d1a11c 100644 --- a/apps/portal/src/app/features/seafarer/pages/SeafarerRegistryPage.tsx +++ b/apps/portal/src/app/features/seafarer/pages/SeafarerRegistryPage.tsx @@ -1,7 +1,7 @@ +import { type StatusTone } from '@ema-platform/shared'; import { useEffect, useState } from 'react'; import { ActionIcon, - Badge, Box, Button, Card, @@ -35,7 +35,7 @@ import { IconX, } from '@tabler/icons-react'; import { useNavigate } from 'react-router-dom'; -import { notify } from '@ema-platform/ui'; +import { StatusBadge, notify } from '@ema-platform/ui'; // --------------------------------------------------------------------------- // Types @@ -163,26 +163,17 @@ function StatCard({ // --------------------------------------------------------------------------- // Status badges // --------------------------------------------------------------------------- -const STATUS_COLOR: Record = { - Active: 'teal', - Pending: 'yellow', - Suspended: 'red', - Expired: 'orange', - Fit: 'teal', - Unfit: 'red', +const STATUS_TONE: Record = { + Active: 'success', + Pending: 'warning', + Suspended: 'danger', + Expired: 'pending', + Fit: 'success', + Unfit: 'danger', }; -function StatusBadge({ value }: { value: string }) { - return ( - - {value} - - ); +function RegistryStatus({ value }: { value: string }) { + return ; } // --------------------------------------------------------------------------- @@ -239,9 +230,9 @@ export function SeafarerRegistryPage() { {s.mobile} {s.region} {s.registeredAt} - - - + + + diff --git a/apps/portal/src/app/features/vessel-registration/pages/OwnershipTransferPage.tsx b/apps/portal/src/app/features/vessel-registration/pages/OwnershipTransferPage.tsx index b4e44e22b..9c2c890a7 100644 --- a/apps/portal/src/app/features/vessel-registration/pages/OwnershipTransferPage.tsx +++ b/apps/portal/src/app/features/vessel-registration/pages/OwnershipTransferPage.tsx @@ -1,9 +1,9 @@ +import { type StatusTone } from '@ema-platform/shared'; import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useApiMutation } from '@ema-platform/api'; import { Alert, - Badge, Box, Button, Card, @@ -31,7 +31,7 @@ import { IconTransferIn, IconUser, } from '@tabler/icons-react'; -import { notify, PhoneInput } from '@ema-platform/ui'; +import { StatusBadge, notify, PhoneInput } from '@ema-platform/ui'; import { isValidPhoneNumber } from 'libphonenumber-js'; // Minimal vessel type for the approved vessel list @@ -117,11 +117,11 @@ const TRANSFER_REASONS = [ 'Other', ]; -const STATUS_COLOR: Record = { - Pending: 'gray', - 'Under Review': 'yellow', - Approved: 'teal', - Rejected: 'red', +const STATUS_TONE: Record = { + Pending: 'neutral', + 'Under Review': 'warning', + Approved: 'success', + Rejected: 'danger', }; // --------------------------------------------------------------------------- @@ -140,7 +140,11 @@ function TransferCard({ req }: { req: OwnershipTransferRequest }) { {req.id} · Transfer to {req.newOwnerName} - {req.status} + diff --git a/apps/portal/src/app/features/vessel-registration/pages/VesselRegistrationPage.tsx b/apps/portal/src/app/features/vessel-registration/pages/VesselRegistrationPage.tsx index 3a0790c61..7f896900b 100644 --- a/apps/portal/src/app/features/vessel-registration/pages/VesselRegistrationPage.tsx +++ b/apps/portal/src/app/features/vessel-registration/pages/VesselRegistrationPage.tsx @@ -1,9 +1,9 @@ +import { type StatusTone } from '@ema-platform/shared'; import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Alert, - Badge, Button, Card, Divider, @@ -27,7 +27,7 @@ import { IconClockHour4, IconTransferIn, } from '@tabler/icons-react'; -import { AdvancedTable } from '@ema-platform/ui'; +import { StatusBadge, AdvancedTable } from '@ema-platform/ui'; import { inFlightColumns } from '../inFlightColumns'; import { TERMINAL_STATUSES, @@ -68,12 +68,12 @@ interface VesselRegistration { expiryDate: string | null; } -const STATUS_COLOR: Record = { - Pending: 'gray', - 'Under Review': 'yellow', - Approved: 'teal', - Rejected: 'red', - 'Correction Required': 'orange', +const STATUS_TONE: Record = { + Pending: 'neutral', + 'Under Review': 'warning', + Approved: 'success', + Rejected: 'danger', + 'Correction Required': 'pending', }; // Inland vessel certificates (1) @@ -281,9 +281,12 @@ export function VesselRegistrationPage() { {registration.id} - - {registration.status} - + diff --git a/apps/portal/src/app/features/vessel-registration/pages/VesselTransferPage.tsx b/apps/portal/src/app/features/vessel-registration/pages/VesselTransferPage.tsx index f0274d0e0..8b4afd42c 100644 --- a/apps/portal/src/app/features/vessel-registration/pages/VesselTransferPage.tsx +++ b/apps/portal/src/app/features/vessel-registration/pages/VesselTransferPage.tsx @@ -1,3 +1,5 @@ +import { StatusBadge } from '@ema-platform/ui'; +import { type StatusTone } from '@ema-platform/shared'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { @@ -30,10 +32,10 @@ import { const TRANSFER_TYPE_KEY = 'VESSEL_OWNERSHIP_TRANSFER'; -const VESSEL_STATUS_COLORS: Record = { - REGISTERED: 'green', - SUSPENDED: 'orange', - DEREGISTERED: 'gray', +const VESSEL_STATUS_TONES: Record = { + REGISTERED: 'success', + SUSPENDED: 'pending', + DEREGISTERED: 'neutral', }; /** @@ -194,12 +196,11 @@ export function VesselTransferPage() { {categoryLabels[vessel.category] ?? vessel.category} - - {vessel.status} - + /> diff --git a/libs/api/src/lib/features/seafarer-registration/seafarer-registration.constants.ts b/libs/api/src/lib/features/seafarer-registration/seafarer-registration.constants.ts index 8a223c3f6..e3406a4f0 100644 --- a/libs/api/src/lib/features/seafarer-registration/seafarer-registration.constants.ts +++ b/libs/api/src/lib/features/seafarer-registration/seafarer-registration.constants.ts @@ -107,12 +107,26 @@ export const SEAFARER_REGISTRATION_STATUS_LABELS: Record = { - DRAFT: 'gray', - SUBMITTED: 'blue', - RESUBMIT_REQUIRED: 'orange', - APPROVED: 'teal', - REJECTED: 'red', +/** + * Registration status → platform tone. + * + * Tones, not colours. `StatusTone` is the platform's status vocabulary and the + * one place a tone becomes a colour (`STATUS_TONE_COLOR` in + * @ema-platform/shared), so this map cannot drift the way `APPROVED: 'teal'` + * had already drifted from every other feature's green success. + * + * The union is repeated rather than imported because @ema-platform/api does not + * depend on the theme layer, and should not start to for five string literals. + */ +export const SEAFARER_REGISTRATION_STATUS_TONES: Record< + SeafarerRegistrationStatus, + 'success' | 'warning' | 'danger' | 'info' | 'pending' | 'neutral' +> = { + DRAFT: 'neutral', + SUBMITTED: 'info', + RESUBMIT_REQUIRED: 'pending', + APPROVED: 'success', + REJECTED: 'danger', }; /** Human label for each answer — the review table and the summary both use it. */ diff --git a/libs/shared/src/lib/theme/base-theme.ts b/libs/shared/src/lib/theme/base-theme.ts index 73315594a..f39fc5d59 100644 --- a/libs/shared/src/lib/theme/base-theme.ts +++ b/libs/shared/src/lib/theme/base-theme.ts @@ -1,3 +1,4 @@ +import type { CSSProperties } from 'react'; import { createTheme, rem } from '@mantine/core'; /** @@ -97,6 +98,40 @@ export const baseTheme = createTheme({ Select: { defaultProps: { radius: 'md' } }, PasswordInput: { defaultProps: { radius: 'md' } }, + // The page sits on a tinted surface and cards float on white. Without this + // the main area is the same white as every Paper on it, and the card + // borders are the only thing separating content from chrome. + AppShell: { + styles: { main: { background: 'var(--ema-surface-page)' } }, + }, + + // Tables — the registry look: quiet uppercase headers, hairline row + // borders, a tint on hover, no zebra striping and no column rules. Set + // once here so the 14 pages rendering a raw match the 28 that go + // through AdvancedTable instead of each picking their own density. + // + // Header text is `text-secondary` rather than the lighter dimmed gray the + // mockup used: at 11px uppercase, gray-5 on white fails 4.5:1. + Table: { + defaultProps: { highlightOnHover: true, verticalSpacing: 'sm', horizontalSpacing: 'md' }, + styles: { + table: { + '--table-border-color': 'var(--ema-border-subtle)', + '--table-hover-color': 'var(--ema-surface-page)', + '--table-striped-color': 'var(--ema-surface-sunken)', + } as CSSProperties, + th: { + fontSize: rem(11), + fontWeight: 700, + textTransform: 'uppercase', + letterSpacing: '0.05em', + color: 'var(--ema-text-secondary)', + whiteSpace: 'nowrap', + }, + td: { fontSize: rem(13) }, + }, + }, + // Mantine's stock scroll wrapper (NativeScrollArea) discards the // max-height it's handed unless scrollAreaComponent is set, so a modal // taller than the viewport just gets clipped with no way to scroll it. diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index d4c0a785b..4c0ab7d1b 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -9,6 +9,7 @@ export * from "./lib/feedback/FeatureUnavailable"; export * from "./lib/feedback/EmptyState"; export * from "./lib/feedback/ErrorState"; export * from "./lib/feedback/PageLoader"; +export * from "./lib/feedback/StatusBadge"; export * from "./lib/components/MaritimeLoader"; export * from "./lib/theme/maritime-loader-theme"; export * from "./lib/layout/AppHeader"; diff --git a/libs/ui/src/lib/data/AdvancedTable.tsx b/libs/ui/src/lib/data/AdvancedTable.tsx index 53a5a54cd..67b92ad59 100644 --- a/libs/ui/src/lib/data/AdvancedTable.tsx +++ b/libs/ui/src/lib/data/AdvancedTable.tsx @@ -51,6 +51,10 @@ interface AdvancedTableProps { rowStyle?: (row: T, index: number) => CSSProperties | undefined; /** Makes rows clickable (adds pointer cursor). */ onRowClick?: (row: T) => void; + /** Card title, top-left. Defaults to `tableName`, which every caller already passes. */ + title?: ReactNode; + /** Search box, filters, export — rendered top-right before Refresh/View. */ + toolbar?: ReactNode; } function getByPath(obj: unknown, path?: string): unknown { @@ -82,6 +86,8 @@ export function AdvancedTable({ verticalSpacing = "sm", rowStyle, onRowClick, + title, + toolbar, }: AdvancedTableProps) { const { t } = useTranslation(); const [visible, setVisible] = useState( @@ -94,13 +100,22 @@ export function AdvancedTable({ }); const shownColumns = columns.filter((_, i) => visible[i] ?? true); + const heading = title ?? tableName; + const from = itemCount === 0 ? 0 : pageIndex * pageSize + 1; + const to = Math.min(itemCount, pageIndex * pageSize + data.length); + return ( - - + + - {""} + {heading && ( + + {heading} + + )} - + + {toolbar} {refresh && (
+
{shownColumns.map((col, i) => ( @@ -230,8 +239,21 @@ export function AdvancedTable({
- {(itemCount > pageSize || onPageSizeChange) && ( - + + + {t("common.showingRange", { + from, + to, + total: itemCount, + defaultValue: "Showing {{from}}–{{to}} of {{total}}", + })} + + {onPageSizeChange && (