diff --git a/apps/portal/src/app/features/documents/pages/DocumentVaultPage.tsx b/apps/portal/src/app/features/documents/pages/DocumentVaultPage.tsx index 82313ad9c..172419552 100644 --- a/apps/portal/src/app/features/documents/pages/DocumentVaultPage.tsx +++ b/apps/portal/src/app/features/documents/pages/DocumentVaultPage.tsx @@ -1,561 +1,21 @@ -import { useState } from 'react'; -import { Link } from 'react-router-dom'; -import { - Alert, - Anchor, - Badge, - Button, - Card, - Container, - Divider, - Group, - Loader, - SimpleGrid, - Stack, - Tabs, - Text, - ThemeIcon, - Title, -} from '@mantine/core'; -import { - IconAnchor, - IconCertificate, - IconEye, - IconHeartbeat, - IconIdBadge2, - IconInfoCircle, - IconPaperclip, -} from '@tabler/icons-react'; +import { Container } from '@mantine/core'; +import { FeatureUnavailable } from '@ema-platform/ui'; import { useTranslation } from 'react-i18next'; -import { useDateDisplayer } from '@ema-platform/shared'; -import { notify, PdfPreviewModal } from '@ema-platform/ui'; -import { - extractErrorMessage, - useGetAttachmentsQuery, - useGetCertificateUrlMutation, - useGetMyLicensesQuery, - useGetMyMedicalCertificatesQuery, - useGetMySeaServiceRecordsQuery, - useGetMySeafarerDocumentsQuery, - useGetMySeafarerRegistrationQuery, - useLazyGetMySeafarerDocumentDownloadQuery, - type SeafarerDocument, - type SeafarerRecordStatus, -} from '@ema-platform/api'; -import { LicenseCard, useRenewLicense } from '../../licensing/components/LicenseCard'; -import { documentSlots } from '../../seafarer-registration/components/RegistrationDocuments'; - -type Preview = { url: string; title: string }; - -const RECORD_STATUS_COLOR: Record = { - SUBMITTED: 'blue', - VERIFIED: 'teal', - REJECTED: 'red', -}; /** - * The files attached to one record, read from the attachment store the record - * owns. One query per card rather than a page-wide fetch: `/attachments` is - * owner-scoped, and RTK Query dedupes and caches them by owner anyway. + * Placeholder until this feature has a backend. + * + * This page previously rendered invented figures/records that were + * indistinguishable from real ones. */ -function RecordFiles({ - ownerType, - ownerId, - onPreview, -}: { - ownerType: 'SEA_SERVICE_RECORD' | 'MEDICAL_CERTIFICATE' | 'SEAFARER_REGISTRATION'; - ownerId: string; - onPreview: (preview: Preview) => void; -}) { - const { t } = useTranslation(); - const { data, isLoading } = useGetAttachmentsQuery({ ownerType, ownerId }); - const files = (data ?? []).flatMap((a) => a.files); - - if (isLoading) return ; - if (files.length === 0) - return ( - - {t('documents.files.none')} - - ); - - return ( - - {files.map((file) => ( - - - {file.url ? ( - onPreview({ url: file.url as string, title: file.originalName })} - > - {file.originalName} - - ) : ( - {file.originalName} - )} - - ))} - - ); -} - -function FieldRow({ label, value }: { label: string; value: string }) { - return ( - - - {label} - - - {value} - - - ); -} - -/** Seaman Book / BTC — issued on their own workflow, not as licences. */ -function IssuedDocumentCard({ - document, - onPreview, -}: { - document: SeafarerDocument; - onPreview: (preview: Preview) => void; -}) { - const { t } = useTranslation(); - const showDate = useDateDisplayer(); - const [download, { isFetching }] = useLazyGetMySeafarerDocumentDownloadQuery(); - const issued = document.status === 'ISSUED'; - - async function open() { - try { - const { url } = await download(document.id).unwrap(); - onPreview({ url, title: t(`documents.kind.${document.kind}`) }); - } catch (err) { - notify.error(extractErrorMessage(err), t('documents.openFailed')); - } - } - - return ( - - - - - - -
- - {t(`documents.kind.${document.kind}`)} - - - {document.documentNumber ?? document.requestNumber} - -
-
- - {t(`documents.documentStatus.${document.status}`, { defaultValue: document.status })} - -
- - - - - {document.issueDate && ( - - )} - {document.expiryDate && ( - - )} - - - -
- ); -} - export function DocumentVaultPage() { const { t } = useTranslation(); - const showDate = useDateDisplayer(); - const [preview, setPreview] = useState(null); - - const { data: licences, isLoading: loadingLicences } = useGetMyLicensesQuery(); - const { data: issuedDocuments } = useGetMySeafarerDocumentsQuery(); - const { data: medicals, isLoading: loadingMedicals } = useGetMyMedicalCertificatesQuery(); - const { data: seaService, isLoading: loadingSeaService } = useGetMySeaServiceRecordsQuery(); - const { data: registrationData, isLoading: loadingRegistration } = - useGetMySeafarerRegistrationQuery(); - - const [getCertificateUrl, { isLoading: isDownloadingCert }] = useGetCertificateUrlMutation(); - const { renewLicense, isRenewing } = useRenewLicense(); - - const registration = registrationData?.registration ?? null; - const { data: registrationFiles } = useGetAttachmentsQuery( - { ownerType: 'SEAFARER_REGISTRATION', ownerId: registration?.id ?? '' }, - { skip: !registration }, - ); - - async function openCertificate(licenseId: string) { - try { - const { url } = await getCertificateUrl(licenseId).unwrap(); - setPreview({ url, title: t('licensing.card.downloadCertificate') }); - } catch (err) { - notify.error(extractErrorMessage(err), t('documents.openFailed')); - } - } - - const licenceItems = licences?.items ?? []; - const issued = [issuedDocuments?.seamanBook, issuedDocuments?.btc].filter( - (d): d is SeafarerDocument => Boolean(d), - ); return ( - -
- {t('documents.title')} - - {t('documents.subtitle')} - -
- - - - }> - {t('documents.tabs.license')} - - }> - {t('documents.tabs.medical')} - - }> - {t('documents.tabs.seaService')} - - }> - {t('documents.tabs.personal')} - - - - {/* ── Licences and EMA-issued documents ───────────────────────── */} - - - {issued.length > 0 && ( -
- - {t('documents.issuedTitle')} - - - {issued.map((document) => ( - - ))} - -
- )} - -
- - {t('documents.licensesTitle')} - - {loadingLicences ? ( - - ) : licenceItems.length === 0 ? ( - - {t('documents.empty.licenses')} - - ) : ( - // Two per row, not three: licence type names run long - // ("Multimodal Transport Operator License") and a third - // column truncates them. - - {licenceItems.map((licence) => ( - openCertificate(licence.id)} - onRenew={() => renewLicense(licence)} - /> - ))} - - )} -
-
-
- - {/* ── Medical certificates ────────────────────────────────────── */} - - {loadingMedicals ? ( - - ) : (medicals ?? []).length === 0 ? ( - - {t('documents.empty.medical')} - - ) : ( - - {(medicals ?? []).map((record) => ( - - - - - - -
- - {record.issuerName} - - {record.certificateNumber && ( - - {t('seaRecords.columns.certNumber', { - number: record.certificateNumber, - })} - - )} -
-
- - {t(`seaRecords.columns.recordStatus.${record.status}`, { - defaultValue: record.status, - })} - -
- - - - - - - - - - - -
- ))} -
- )} -
- - {/* ── Sea service records ─────────────────────────────────────── */} - - {loadingSeaService ? ( - - ) : (seaService ?? []).length === 0 ? ( - - {t('documents.empty.seaService')} - - ) : ( - - {(seaService ?? []).map((record) => ( - - - - - - -
- - {record.vesselName} - - {record.imoNumber && ( - - {t('seaRecords.columns.imo', { number: record.imoNumber })} - - )} -
-
- - {t(`seaRecords.columns.recordStatus.${record.status}`, { - defaultValue: record.status, - })} - -
- - - - - - - - - - - -
- ))} -
- )} -
- - {/* ── Personal documents from the seafarer registration ───────── */} - - {loadingRegistration ? ( - - ) : !registration ? ( - // Personal documents are uploaded against a registration, so - // without one there is nothing to show — and nowhere to put an - // upload either. - }> - - {t('documents.personal.noRegistration')} - - - - ) : ( - - - {t('documents.personal.description')} - - - {documentSlots( - Boolean(registration.passportNumber), - registration.nationality, - ).map((slot) => { - const attachment = (registrationFiles ?? []).find( - (a) => a.documentKey === slot.key, - ); - const files = attachment?.files ?? []; - return ( - - -
- - {slot.name} - - {slot.description && ( - - {slot.description} - - )} -
- - {files.length - ? t('documents.personal.uploaded') - : t('documents.personal.missing')} - -
- - - - {files.length === 0 ? ( - - {t('documents.files.none')} - - ) : ( - - {files.map((file) => ( - - - {file.url ? ( - - setPreview({ - url: file.url as string, - title: file.originalName, - }) - } - > - {file.originalName} - - ) : ( - {file.originalName} - )} - - ))} - - )} -
- ); - })} -
-
- )} -
-
-
- - setPreview(null)} - url={preview?.url ?? ''} - title={preview?.title} +
);