diff --git a/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx b/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx index b072a9ecd..5a1235ede 100644 --- a/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx +++ b/apps/portal/src/app/features/certificates/pages/CertificatesPage.tsx @@ -30,43 +30,80 @@ import { IconShieldCheck, } from '@tabler/icons-react'; import { authStorage } from '@ema-platform/auth'; +import { useApiQuery } from '@ema-platform/api'; // --------------------------------------------------------------------------- // Mock data // --------------------------------------------------------------------------- -const MOCK_COC_APPS = [ - { - id: 'COC-APP-2025-001', - type: 'CoC — STCW II/1 Officer in Charge of Navigational Watch', - submitted: '2025-03-10', - examDate: '2025-04-15', - examVenue: 'EMA HQ — Addis Ababa', - status: 'Examination Scheduled', - statusColor: 'indigo', - statusNote: 'TRB inspected and approved by EMA officer. Attend your scheduled examination.', - }, - { - id: 'COC-APP-2025-005', - type: 'CoC — STCW II/5 Able Seafarer Deck (AB)', - submitted: '2025-05-01', - examDate: null, - examVenue: null, - status: 'TRB Inspection', - statusColor: 'yellow', - statusNote: 'Your TRB is being physically inspected by an EMA officer. You may be contacted to bring the original document.', - }, -]; +/** What `/certificates/my` returns: what is held, and what is still in flight. */ +interface CertificatesOverview { + certificates: { + id: string; + type: string; + issued: string; + expiry: string; + status: string; + }[]; + applications: { + id: string; + applicationId: string; + type: string; + submitted: string; + status: string; + }[]; +} -const MOCK_CERTIFICATES = [ - { - id: 'COC-2023-0042', - type: 'CoC — STCW II/1', - issued: '2023-06-20', - expiry: '2028-06-20', - status: 'Valid', - statusColor: 'teal', - }, -]; +/** + * Badge colour per workflow status. + * + * Keyed by the values the API reports rather than display strings, so an + * unmapped status falls back to grey instead of rendering colourless. + */ +const STATUS_COLOR: Record = { + DRAFT: 'gray', + SUBMITTED: 'blue', + UNDER_REVIEW: 'yellow', + UNDER_EVALUATION: 'yellow', + RESUBMIT_REQUIRED: 'orange', + INSPECTION_PENDING: 'grape', + INSPECTION_COMPLETED: 'grape', + ELIGIBILITY_APPROVED: 'teal', + EXAM_PAYMENT_PENDING: 'orange', + EXAM_PAID: 'blue', + EXAM_SCHEDULED: 'indigo', + EXAM_PASSED: 'teal', + EXAM_FAILED: 'red', + APPROVED: 'teal', + REJECTED: 'red', + PAYMENT_PENDING: 'orange', + PAID: 'blue', + PAYMENT_CONFIRMED: 'blue', + CERTIFICATE_ISSUED: 'teal', + COMPLETED: 'teal', + ACTIVE: 'teal', + EXPIRED: 'red', + SUSPENDED: 'orange', + CANCELLED: 'gray', + SUPERSEDED: 'gray', +}; + +/** Turns `EXAM_PAYMENT_PENDING` into something a person reads. */ +function humanStatus(status: string): string { + return status + .toLowerCase() + .split('_') + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' '); +} + +function formatDate(value: string | null | undefined): string { + if (!value) return '—'; + return new Date(value).toLocaleDateString('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + }); +} const API_BASE = (import.meta as { env?: Record }).env?.['VITE_BASE_API_URL'] ?? @@ -99,6 +136,13 @@ export function CertificatesPage() { const [previewTitle, setPreviewTitle] = useState(''); const [loading, setLoading] = useState(false); + const { data } = useApiQuery({ + url: '/certificates/my', + method: 'GET', + }); + const certificates = data?.certificates ?? []; + const applications = data?.applications ?? []; + const openPreview = async (profileId: string, title: string) => { setLoading(true); try { @@ -179,7 +223,7 @@ export function CertificatesPage() { {/* Active applications */} My Applications - {MOCK_COC_APPS.length === 0 ? ( + {applications.length === 0 ? ( }> No active CoC/CoP applications. Click "Apply for CoC / CoP" to start. @@ -193,21 +237,34 @@ export function CertificatesPage() { - {MOCK_COC_APPS.map((app) => ( + {applications.map((app) => ( {app.id} {app.type} - {app.submitted} + {formatDate(app.submitted)} - {app.examDate - ? <>{app.examDate}{app.examVenue} - : {app.statusNote}} + + {humanStatus(app.status)} + - {app.status} + + {humanStatus(app.status)} + - Details + navigate(`/applications/${app.applicationId}`)} + > + Details + ))} @@ -219,13 +276,13 @@ export function CertificatesPage() { {/* Issued certificates */} My Certificates - {MOCK_CERTIFICATES.length === 0 ? ( + {certificates.length === 0 ? ( }> No certificates issued yet. ) : ( - {MOCK_CERTIFICATES.map((cert) => ( + {certificates.map((cert) => ( @@ -235,7 +292,7 @@ export function CertificatesPage() { {cert.id} - {cert.status} + {cert.status} diff --git a/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx b/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx index 0a26ebb7e..e42de8f64 100644 --- a/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx +++ b/apps/portal/src/app/features/endorsement/pages/EndorsementPage.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useApiQuery } from '@ema-platform/api'; import { useNavigate } from 'react-router-dom'; import { Alert, @@ -39,32 +40,64 @@ import { // --------------------------------------------------------------------------- // Mock data — existing endorsement applications // --------------------------------------------------------------------------- -const MOCK_ENDORSEMENTS = [ - { - id: 'END-APP-2025-001', - cocType: 'Officer in Charge of a Navigational Watch (STCW II/1)', - foreignCocNo: 'PHL-COC-2022-0045', - issuingCountry: 'Philippines', - submitted: '2025-04-05', - status: 'Document Verification', - statusColor: 'blue', - statusNote: 'EMA is verifying your documents. You will be notified when verification is complete.', - }, -]; +/** What `/endorsements/my` returns. */ +interface EndorsementsOverview { + issued: { + id: string; + endorsementNo: string; + cocType: string; + foreignCocNo: string; + issuingCountry: string; + issued: string; + expiry: string; + status: string; + }[]; + applications: { + id: string; + applicationId: string; + cocType: string; + foreignCocNo: string; + issuingCountry: string; + submitted: string; + status: string; + }[]; +} -const MOCK_ISSUED = [ - { - id: 'EMA-END-2024-012', - cocType: 'Chief Mate — STCW II/2', - foreignCocNo: 'GRC-COC-2019-0033', - issuingCountry: 'Greece', - endorsementNo: 'EMA-END-2024-012', - issued: '2024-08-10', - expiry: '2029-06-15', - status: 'Valid', - statusColor: 'teal', - }, -]; +// Keyed by the workflow's own status values so an unmapped one falls back to +// grey rather than rendering colourless. +const STATUS_COLOR: Record = { + DRAFT: 'gray', + SUBMITTED: 'blue', + UNDER_REVIEW: 'yellow', + UNDER_EVALUATION: 'yellow', + RESUBMIT_REQUIRED: 'orange', + APPROVED: 'teal', + REJECTED: 'red', + PAYMENT_PENDING: 'orange', + PAYMENT_CONFIRMED: 'blue', + CERTIFICATE_ISSUED: 'teal', + COMPLETED: 'teal', + ACTIVE: 'teal', + EXPIRED: 'red', + SUSPENDED: 'orange', +}; + +function humanStatus(status: string): string { + return status + .toLowerCase() + .split('_') + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' '); +} + +function formatDate(value: string | null | undefined): string { + if (!value) return '—'; + return new Date(value).toLocaleDateString('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + }); +} // blank PDF const BLANK_PDF = 'data:application/pdf;base64,JVBERi0xLjQKJcfsj6IKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PgplbmRvYmoKMyAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDIgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagp4cmVmCjAgNAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA1OCAwMDAwMCBuIAowMDAwMDAwMTE1IDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSA0L1Jvb3QgMSAwIFI+PgpzdGFydHhyZWYKMjE3CiUlRU9G'; @@ -298,6 +331,13 @@ export function EndorsementPage() { const [applying, setApplying] = useState(false); const [previewId, setPreviewId] = useState(null); + const { data } = useApiQuery({ + url: '/endorsements/my', + method: 'GET', + }); + const endorsementApps = data?.applications ?? []; + const issuedEndorsements = data?.issued ?? []; + if (applying) { return ( @@ -353,29 +393,29 @@ export function EndorsementPage() { {/* Active applications */} My Endorsement Applications - {MOCK_ENDORSEMENTS.length === 0 ? ( + {endorsementApps.length === 0 ? ( }> No active endorsement applications. ) : ( - {MOCK_ENDORSEMENTS.map((app) => ( + {endorsementApps.map((app) => (
{app.cocType} - CoC No: {app.foreignCocNo} · {app.issuingCountry} · Submitted {app.submitted} + CoC No: {app.foreignCocNo} · {app.issuingCountry} · Submitted {formatDate(app.submitted)}
- {app.status} + {humanStatus(app.status)} {app.id}
- } p="xs" mt="sm"> - {app.statusNote} + } p="xs" mt="sm"> + {humanStatus(app.status)}
))} @@ -386,13 +426,13 @@ export function EndorsementPage() { {/* Issued endorsements */} My Endorsements - {MOCK_ISSUED.length === 0 ? ( + {issuedEndorsements.length === 0 ? ( }> No endorsements issued yet. ) : ( - {MOCK_ISSUED.map((end) => ( + {issuedEndorsements.map((end) => ( @@ -402,7 +442,7 @@ export function EndorsementPage() { {end.endorsementNo} - {end.status} + {humanStatus(end.status)}