mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
import { useRef, useState } from 'react';
|
|
import { Alert, Badge, Button, Card, FileButton, Group, Loader, Stack, Text } from '@mantine/core';
|
|
import { IconAlertTriangle, IconCheck, IconFileUpload } from '@tabler/icons-react';
|
|
import {
|
|
SEAFARER_REGISTRATION_DOCUMENTS,
|
|
isEthiopianNationality,
|
|
uploadDocument,
|
|
type Attachment,
|
|
} from '@ema-platform/api';
|
|
|
|
const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
|
|
|
|
/** The document slots a registration asks for. */
|
|
export function documentSlots(passportDeclared: boolean, nationality?: string | null) {
|
|
const ethiopian = isEthiopianNationality(nationality);
|
|
return SEAFARER_REGISTRATION_DOCUMENTS.map((d) => ({
|
|
...d,
|
|
isRequired: d.required === 'passport' ? passportDeclared : d.required === 'ethiopian' ? ethiopian : d.required,
|
|
})).filter((d) => (d.required !== 'passport' || passportDeclared) && (d.required !== 'ethiopian' || ethiopian));
|
|
}
|
|
|
|
export function RegistrationDocuments({
|
|
registrationId,
|
|
passportDeclared,
|
|
nationality,
|
|
attachments,
|
|
readOnly,
|
|
onUploaded,
|
|
}: {
|
|
registrationId: string;
|
|
passportDeclared: boolean;
|
|
nationality?: string | null;
|
|
attachments: Attachment[];
|
|
readOnly?: boolean;
|
|
onUploaded: () => void;
|
|
}) {
|
|
const [busy, setBusy] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const resetRefs = useRef<Record<string, () => void>>({});
|
|
|
|
async function handle(documentKey: string, file: File | null) {
|
|
if (!file) return;
|
|
if (file.size > MAX_FILE_SIZE_BYTES) {
|
|
setError(`File exceeds 5MB limit (${(file.size / 1024 / 1024).toFixed(1)}MB).`);
|
|
resetRefs.current[documentKey]?.();
|
|
return;
|
|
}
|
|
setBusy(documentKey);
|
|
setError(null);
|
|
const result = await uploadDocument({
|
|
ownerType: 'SEAFARER_REGISTRATION',
|
|
ownerId: registrationId,
|
|
documentKey,
|
|
file,
|
|
});
|
|
setBusy(null);
|
|
resetRefs.current[documentKey]?.();
|
|
if (result.ok) onUploaded();
|
|
else setError(result.error);
|
|
}
|
|
|
|
return (
|
|
<Stack gap="sm">
|
|
{error && (
|
|
<Alert color="red" icon={<IconAlertTriangle size={16} />}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
{documentSlots(passportDeclared, nationality).map((slot) => {
|
|
const existing = attachments.find((a) => a.documentKey === slot.key);
|
|
const uploaded = Boolean(existing?.files?.length);
|
|
return (
|
|
<Card
|
|
key={slot.key}
|
|
withBorder
|
|
padding="md"
|
|
style={{
|
|
borderColor: uploaded ? 'var(--mantine-color-teal-4)' : undefined,
|
|
borderStyle: uploaded ? 'solid' : 'dashed',
|
|
}}
|
|
>
|
|
<Group justify="space-between" wrap="nowrap" align="flex-start">
|
|
<div style={{ minWidth: 0 }}>
|
|
<Group gap="xs">
|
|
<Text fw={600} size="sm">
|
|
{slot.name}
|
|
</Text>
|
|
{!slot.isRequired && (
|
|
<Badge size="xs" variant="light" color="gray">
|
|
optional
|
|
</Badge>
|
|
)}
|
|
{uploaded && (
|
|
<Badge size="xs" color="teal" leftSection={<IconCheck size={10} />}>
|
|
uploaded
|
|
</Badge>
|
|
)}
|
|
</Group>
|
|
{slot.description && (
|
|
<Text size="xs" c="dimmed" mt={2}>
|
|
{slot.description}
|
|
</Text>
|
|
)}
|
|
{existing?.files?.[0] && (
|
|
<Text size="xs" c="dimmed" truncate mt={2}>
|
|
{existing.files[0].originalName} · {(existing.files[0].sizeBytes / 1024).toFixed(0)} KB
|
|
</Text>
|
|
)}
|
|
</div>
|
|
<Group gap="xs" wrap="nowrap">
|
|
{existing?.files?.[0]?.url && (
|
|
<Button size="xs" variant="subtle" component="a" href={existing.files[0].url} target="_blank">
|
|
View
|
|
</Button>
|
|
)}
|
|
{!readOnly && (
|
|
<FileButton
|
|
resetRef={(r) => {
|
|
if (r) resetRefs.current[slot.key] = r;
|
|
}}
|
|
onChange={(file) => handle(slot.key, file)}
|
|
accept={slot.accept}
|
|
>
|
|
{(props) => (
|
|
<Button
|
|
{...props}
|
|
size="xs"
|
|
variant={uploaded ? 'light' : 'filled'}
|
|
leftSection={busy === slot.key ? <Loader size={12} /> : <IconFileUpload size={14} />}
|
|
disabled={busy === slot.key}
|
|
>
|
|
{uploaded ? 'Replace' : 'Upload'}
|
|
</Button>
|
|
)}
|
|
</FileButton>
|
|
)}
|
|
</Group>
|
|
</Group>
|
|
</Card>
|
|
);
|
|
})}
|
|
</Stack>
|
|
);
|
|
}
|