mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-09 03:38:18 +00:00
ui componenet based on the requirements
This commit is contained in:
@@ -0,0 +1,621 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Group,
|
||||
Modal,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
Textarea,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
rem,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconArrowLeft,
|
||||
IconBook2,
|
||||
IconCalendar,
|
||||
IconCertificate,
|
||||
IconCheck,
|
||||
IconCircleCheck,
|
||||
IconCreditCard,
|
||||
IconExternalLink,
|
||||
IconEye,
|
||||
IconFileDescription,
|
||||
IconInfoCircle,
|
||||
IconShieldCheck,
|
||||
IconUser,
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { MOCK_COC_APPS, STATUS_COLOR } from './CoCQueuePage';
|
||||
import type { CoCApp, CoCAppStatus } from './CoCQueuePage';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Demo doc viewer (same pattern as ApplicationReviewPage)
|
||||
// ---------------------------------------------------------------------------
|
||||
const DEMO_PDF = 'data:application/pdf;base64,JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PgplbmRvYmoKMyAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDIgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagp4cmVmCjAgNAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA1OCAwMDAwMCBuIAowMDAwMDAwMTE1IDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSA0L1Jvb3QgMSAwIFI+PgpzdGFydHhyZWYKMjIwCiUlRU9G';
|
||||
|
||||
function getDocUrl(fileName: string) {
|
||||
if (!fileName) return '';
|
||||
return fileName.endsWith('.pdf') ? DEMO_PDF : `https://placehold.co/600x400/e9ecef/6c757d?text=${encodeURIComponent(fileName)}`;
|
||||
}
|
||||
|
||||
function DocViewer({ fileName, label }: { fileName: string; label: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const isPdf = fileName.endsWith('.pdf');
|
||||
const url = getDocUrl(fileName);
|
||||
if (!fileName) return <Badge size="xs" color="red" variant="light">Not uploaded</Badge>;
|
||||
return (
|
||||
<>
|
||||
<Card withBorder radius="md" p="sm">
|
||||
<Group gap="sm" wrap="nowrap" mb="sm">
|
||||
<ThemeIcon size="md" variant="light" color={isPdf ? 'red' : 'blue'} radius="md">
|
||||
<IconFileDescription size={16} stroke={1.5} />
|
||||
</ThemeIcon>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text fz="sm" fw={600}>{label}</Text>
|
||||
<Text fz="xs" c="dimmed" truncate>{fileName}</Text>
|
||||
</div>
|
||||
<Badge size="xs" variant="light" color={isPdf ? 'red' : 'blue'}>{isPdf ? 'PDF' : 'IMG'}</Badge>
|
||||
</Group>
|
||||
<Box style={{ width: '100%', height: rem(180), borderRadius: rem(6), overflow: 'hidden', border: '1px solid var(--mantine-color-default-border)', background: 'var(--mantine-color-gray-0)' }}>
|
||||
{isPdf
|
||||
? <iframe src={url} style={{ width: '100%', height: '100%', border: 'none' }} title={fileName} />
|
||||
: <img src={url} alt={fileName} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />}
|
||||
</Box>
|
||||
<Button size="xs" variant="subtle" fullWidth mt="xs" leftSection={<IconEye size={13} />} rightSection={<IconExternalLink size={13} />} onClick={() => setOpen(true)}>
|
||||
View Full Document
|
||||
</Button>
|
||||
</Card>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Text fw={700}>{label} — {fileName}</Text>} size="90vw" styles={{ body: { padding: 0, height: '80vh' } }}>
|
||||
{isPdf
|
||||
? <iframe src={url} style={{ width: '100%', height: '100%', border: 'none' }} title={fileName} />
|
||||
: <img src={url} alt={fileName} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div>
|
||||
<Text fz="xs" c="dimmed" tt="uppercase" fw={600} lh={1.2}>{label}</Text>
|
||||
<Text fz="sm" mt={2}>{value || '—'}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionLabel({ children }: { children: string }) {
|
||||
return <Text fw={600} fz="xs" tt="uppercase" c="gray.6" mb="sm">{children}</Text>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Workflow progress bar
|
||||
// ---------------------------------------------------------------------------
|
||||
const STEP_ORDER: CoCAppStatus[] = [
|
||||
'Submitted', 'Document Review', 'TRB Inspection',
|
||||
'Examination Scheduled', 'Examination Passed', 'Certificate Issued',
|
||||
];
|
||||
|
||||
function WorkflowBar({ status }: { status: CoCAppStatus }) {
|
||||
const activeIdx = STEP_ORDER.indexOf(status);
|
||||
const isBranch = ['TRB Rejected', 'TRB Resubmit Required', 'Examination Failed'].includes(status);
|
||||
const labels = ['Submitted', 'Doc Review', 'TRB Inspect', 'Exam Scheduled', 'Exam Passed', 'Issued'];
|
||||
return (
|
||||
<Paper withBorder radius="md" p="md" mb="md">
|
||||
<Text fz="xs" c="dimmed" fw={700} tt="uppercase" mb="xs">Application Progress</Text>
|
||||
<Group gap={0} align="flex-start" wrap="nowrap">
|
||||
{STEP_ORDER.map((s, i) => {
|
||||
const done = isBranch ? false : activeIdx > i;
|
||||
const current = !isBranch && activeIdx === i;
|
||||
return (
|
||||
<Group key={s} gap={0} align="center" style={{ flex: i < STEP_ORDER.length - 1 ? 1 : 'none' }}>
|
||||
<Stack gap={2} align="center" style={{ minWidth: rem(52) }}>
|
||||
<Box style={{
|
||||
width: rem(30), height: rem(30), borderRadius: '50%',
|
||||
background: done ? 'var(--mantine-color-teal-6)' : current ? 'var(--mantine-color-blue-6)' : 'var(--mantine-color-gray-2)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
||||
}}>
|
||||
{done
|
||||
? <IconCheck size={14} color="white" stroke={2.5} />
|
||||
: <Text fz="xs" fw={700} c={current ? 'white' : 'gray.5'}>{i + 1}</Text>}
|
||||
</Box>
|
||||
<Text fz={9} fw={current || done ? 700 : 400} c={done ? 'teal.7' : current ? 'blue.7' : 'dimmed'} ta="center" lh={1.2}>{labels[i]}</Text>
|
||||
</Stack>
|
||||
{i < STEP_ORDER.length - 1 && (
|
||||
<Box style={{ flex: 1, height: rem(2), background: done ? 'var(--mantine-color-teal-4)' : 'var(--mantine-color-gray-2)', marginBottom: rem(20) }} />
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
})}
|
||||
</Group>
|
||||
{isBranch && (
|
||||
<Alert variant="light" color="red" icon={<IconAlertTriangle size={13} />} p="xs" mt="xs">
|
||||
<Text fz="xs" fw={600}>
|
||||
{status === 'TRB Resubmit Required' && 'TRB resubmission required — applicant notified.'}
|
||||
{status === 'TRB Rejected' && 'TRB rejected — applicant must submit a new application.'}
|
||||
{status === 'Examination Failed' && 'Examination failed — applicant must submit a new application.'}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Action modal
|
||||
// ---------------------------------------------------------------------------
|
||||
const EXAM_VENUES = [
|
||||
{ value: 'addis', label: 'EMA HQ — Addis Ababa' },
|
||||
{ value: 'regional', label: 'EMA Regional Office — Djibouti Liaison' },
|
||||
];
|
||||
|
||||
type ActionKey = 'advance_trb' | 'request_docs' | 'approve_trb' | 'resubmit_trb' | 'reject_trb' | 'exam_pass' | 'exam_fail' | 'issue_cert';
|
||||
|
||||
const ACTION_TO_STATUS: Record<ActionKey, CoCAppStatus> = {
|
||||
advance_trb: 'TRB Inspection',
|
||||
request_docs: 'Document Review',
|
||||
approve_trb: 'Examination Scheduled',
|
||||
resubmit_trb: 'TRB Resubmit Required',
|
||||
reject_trb: 'TRB Rejected',
|
||||
exam_pass: 'Examination Passed',
|
||||
exam_fail: 'Examination Failed',
|
||||
issue_cert: 'Certificate Issued',
|
||||
};
|
||||
|
||||
function getActions(status: CoCAppStatus): { value: ActionKey; label: string }[] {
|
||||
switch (status) {
|
||||
case 'Submitted':
|
||||
case 'Document Review':
|
||||
return [
|
||||
{ value: 'advance_trb', label: 'Documents OK — Move to TRB Inspection' },
|
||||
{ value: 'request_docs', label: 'Request Additional Documents' },
|
||||
];
|
||||
case 'TRB Inspection':
|
||||
case 'TRB Resubmit Required':
|
||||
return [
|
||||
{ value: 'approve_trb', label: 'TRB Approved — Schedule Examination' },
|
||||
{ value: 'resubmit_trb', label: 'Request TRB Resubmission' },
|
||||
{ value: 'reject_trb', label: 'Reject TRB (Close Application)' },
|
||||
];
|
||||
case 'Examination Scheduled':
|
||||
return [
|
||||
{ value: 'exam_pass', label: 'Record Result — Passed' },
|
||||
{ value: 'exam_fail', label: 'Record Result — Failed' },
|
||||
];
|
||||
case 'Examination Passed':
|
||||
return [{ value: 'issue_cert', label: 'Issue Certificate' }];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function ActionModal({
|
||||
app,
|
||||
opened,
|
||||
onClose,
|
||||
onAction,
|
||||
}: {
|
||||
app: CoCApp;
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onAction: (newStatus: CoCAppStatus, notes: string, examDate?: string, examVenue?: string) => void;
|
||||
}) {
|
||||
const [action, setAction] = useState<string | null>(null);
|
||||
const [notes, setNotes] = useState('');
|
||||
const [examDate, setExamDate] = useState('');
|
||||
const [venue, setVenue] = useState<string | null>(null);
|
||||
|
||||
const actionOptions = getActions(app.status);
|
||||
const needsExam = action === 'approve_trb';
|
||||
const isDestructive = action === 'reject_trb' || action === 'exam_fail';
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (!action) return;
|
||||
if (needsExam && (!examDate || !venue)) {
|
||||
notify.error('Please set an examination date and venue.'); return;
|
||||
}
|
||||
const newStatus = ACTION_TO_STATUS[action as ActionKey];
|
||||
const venueLabel = EXAM_VENUES.find(v => v.value === venue)?.label ?? venue ?? undefined;
|
||||
onAction(newStatus, notes, examDate || undefined, venueLabel);
|
||||
setAction(null); setNotes(''); setExamDate(''); setVenue(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={<Group gap="xs"><IconShieldCheck size={17} /><Text fw={700}>Officer Action</Text></Group>}
|
||||
size="md"
|
||||
radius="lg"
|
||||
>
|
||||
<Stack gap="md">
|
||||
{actionOptions.length === 0 ? (
|
||||
<Alert variant="light" color="teal" icon={<IconCircleCheck size={15} />}>
|
||||
<Text fz="sm" fw={600}>No further actions available for this status.</Text>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Select
|
||||
label="Action"
|
||||
placeholder="Select an action…"
|
||||
data={actionOptions}
|
||||
value={action}
|
||||
onChange={setAction}
|
||||
size="sm"
|
||||
/>
|
||||
{needsExam && (
|
||||
<SimpleGrid cols={2} spacing="sm">
|
||||
<TextInput label="Examination Date" type="date" value={examDate} onChange={(e) => setExamDate(e.currentTarget.value)} leftSection={<IconCalendar size={14} />} size="sm" required />
|
||||
<Select label="Venue" placeholder="Select venue" data={EXAM_VENUES} value={venue} onChange={setVenue} size="sm" required />
|
||||
</SimpleGrid>
|
||||
)}
|
||||
<Textarea
|
||||
label="Notes / Remarks (sent to applicant)"
|
||||
placeholder="TRB inspection findings, exam result details, rejection reason…"
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.currentTarget.value)}
|
||||
rows={3}
|
||||
size="sm"
|
||||
/>
|
||||
{isDestructive && (
|
||||
<Alert variant="light" color="red" icon={<IconAlertTriangle size={15} />} p="sm">
|
||||
<Text fz="xs" fw={600}>
|
||||
{action === 'reject_trb' && 'This will permanently close the application. The applicant must submit a new application.'}
|
||||
{action === 'exam_fail' && 'This will record an examination failure. The applicant must apply again to re-attempt.'}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={onClose}>Cancel</Button>
|
||||
<Button
|
||||
leftSection={<IconCheck size={14} />}
|
||||
disabled={!action}
|
||||
color={isDestructive ? 'red' : action === 'issue_cert' || action === 'approve_trb' || action === 'exam_pass' ? 'teal' : 'blue'}
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Group>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main review page
|
||||
// ---------------------------------------------------------------------------
|
||||
export function CoCReviewPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const original = MOCK_COC_APPS.find((a) => a.id === id);
|
||||
|
||||
const [app, setApp] = useState<CoCApp | null>(original ?? null);
|
||||
const [actionModalOpen, setActionModalOpen] = useState(false);
|
||||
|
||||
if (!app) {
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Button variant="subtle" leftSection={<IconArrowLeft size={15} />} w="fit-content" onClick={() => navigate('/coc-queue')}>Back to Queue</Button>
|
||||
<Alert variant="light" color="red" icon={<IconAlertTriangle size={17} />}>Application not found.</Alert>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const handleAction = (newStatus: CoCAppStatus, notes: string, examDate?: string, examVenue?: string) => {
|
||||
const trbStatuses: CoCAppStatus[] = ['TRB Inspection', 'TRB Resubmit Required', 'TRB Rejected', 'Examination Scheduled'];
|
||||
const examStatuses: CoCAppStatus[] = ['Examination Passed', 'Examination Failed'];
|
||||
setApp((prev) => {
|
||||
if (!prev) return prev;
|
||||
return {
|
||||
...prev,
|
||||
status: newStatus,
|
||||
examDate: examDate ?? prev.examDate,
|
||||
examVenue: examVenue ?? prev.examVenue,
|
||||
trbInspectionNotes: trbStatuses.includes(newStatus) ? notes || prev.trbInspectionNotes : prev.trbInspectionNotes,
|
||||
examNotes: examStatuses.includes(newStatus) ? notes || prev.examNotes : prev.examNotes,
|
||||
officerRemarks: notes || prev.officerRemarks,
|
||||
};
|
||||
});
|
||||
const labels: Record<CoCAppStatus, string> = {
|
||||
'Submitted': 'moved to Submitted',
|
||||
'Document Review': 'moved to Document Review',
|
||||
'TRB Inspection': 'moved to TRB Inspection',
|
||||
'TRB Resubmit Required': 'TRB resubmission requested',
|
||||
'TRB Rejected': 'TRB rejected — application closed',
|
||||
'Examination Scheduled': `examination scheduled for ${examDate}`,
|
||||
'Examination Passed': 'examination result recorded — Passed',
|
||||
'Examination Failed': 'examination result recorded — Failed',
|
||||
'Certificate Issued': 'certificate issued',
|
||||
};
|
||||
notify.success(`Application ${labels[newStatus] ?? newStatus}.`);
|
||||
};
|
||||
|
||||
const allDocs = app.competencyDocs.every((d) => d.uploaded);
|
||||
const actionOptions = getActions(app.status);
|
||||
const isTerminal = ['TRB Rejected', 'Examination Failed', 'Certificate Issued'].includes(app.status);
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Header */}
|
||||
<Group justify="space-between">
|
||||
<Group gap="sm">
|
||||
<ActionIcon variant="subtle" size="lg" onClick={() => navigate('/coc-queue')}>
|
||||
<IconArrowLeft size={18} />
|
||||
</ActionIcon>
|
||||
<div>
|
||||
<Title order={3}>CoC / CoP Application Review</Title>
|
||||
<Group gap={6} mt={2}>
|
||||
<Text fz="sm" c="dimmed">{app.id}</Text>
|
||||
<Text fz="sm" c="dimmed">·</Text>
|
||||
<Text fz="sm" c="dimmed">Submitted {app.submitted}</Text>
|
||||
</Group>
|
||||
</div>
|
||||
</Group>
|
||||
<Badge size="lg" variant="light" color={STATUS_COLOR[app.status]}>{app.status}</Badge>
|
||||
</Group>
|
||||
|
||||
{/* Action bar */}
|
||||
{!isTerminal && actionOptions.length > 0 && (
|
||||
<Paper withBorder radius="lg" p="md" bg="gray.0">
|
||||
<Group gap="sm" justify="flex-end">
|
||||
<Text fz="sm" c="dimmed" style={{ flex: 1 }}>Review all tabs, then take an action:</Text>
|
||||
<Button size="sm" leftSection={<IconShieldCheck size={15} />} onClick={() => setActionModalOpen(true)}>
|
||||
Take Action
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{isTerminal && (
|
||||
<Alert
|
||||
variant="light"
|
||||
color={app.status === 'Certificate Issued' ? 'teal' : 'red'}
|
||||
icon={app.status === 'Certificate Issued' ? <IconCircleCheck size={17} /> : <IconAlertTriangle size={17} />}
|
||||
>
|
||||
<Text fz="sm" fw={600}>
|
||||
{app.status === 'Certificate Issued' && 'Certificate has been issued and dispatched to the seafarer.'}
|
||||
{app.status === 'TRB Rejected' && 'TRB has been rejected. This application is closed. Applicant must re-apply.'}
|
||||
{app.status === 'Examination Failed' && 'Examination failed. This application is closed. Applicant must re-apply.'}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<WorkflowBar status={app.status} />
|
||||
|
||||
<Tabs defaultValue="profile" variant="outline" radius="md">
|
||||
<Tabs.List mb="md">
|
||||
<Tabs.Tab value="profile" leftSection={<IconUser size={16} />}>Applicant</Tabs.Tab>
|
||||
<Tabs.Tab value="competency" leftSection={<IconFileDescription size={16} />}>Competency Docs</Tabs.Tab>
|
||||
<Tabs.Tab value="trb" leftSection={<IconBook2 size={16} />}>TRB Inspection</Tabs.Tab>
|
||||
<Tabs.Tab value="exam" leftSection={<IconCertificate size={16} />}>Examination</Tabs.Tab>
|
||||
<Tabs.Tab value="payment" leftSection={<IconCreditCard size={16} />}>Payment</Tabs.Tab>
|
||||
<Tabs.Tab value="issuance" leftSection={<IconShieldCheck size={16} />}>Issuance</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
{/* ── Applicant ─────────────────────────────────────────── */}
|
||||
<Tabs.Panel value="profile">
|
||||
<Paper withBorder radius="lg" p="xl">
|
||||
<Group gap="sm" mb="lg">
|
||||
<ThemeIcon size="xl" variant="light" radius="lg"><IconUser size={22} stroke={1.5} /></ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700} fz="lg">{app.name}</Text>
|
||||
<Text fz="sm" c="dimmed">{app.seafarerId}</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Divider mb="md" />
|
||||
<SectionLabel>Personal Information</SectionLabel>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md" mb="lg">
|
||||
<InfoRow label="Email" value={app.email} />
|
||||
<InfoRow label="Mobile" value={app.mobile} />
|
||||
<InfoRow label="Nationality" value={app.nationality} />
|
||||
</SimpleGrid>
|
||||
<Divider mb="md" />
|
||||
<SectionLabel>Certificate Applied For</SectionLabel>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
||||
<InfoRow label="Certificate" value={app.certType} />
|
||||
<InfoRow label="STCW Reference" value={app.stcwRef} />
|
||||
<InfoRow label="Level" value={app.level} />
|
||||
<InfoRow label="Department" value={app.dept} />
|
||||
<InfoRow label="Submitted" value={app.submitted} />
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
</Tabs.Panel>
|
||||
|
||||
{/* ── Competency Documents ──────────────────────────────── */}
|
||||
<Tabs.Panel value="competency">
|
||||
<Stack gap="md">
|
||||
<Alert variant="light" color={allDocs ? 'teal' : 'orange'} icon={allDocs ? <IconCircleCheck size={17} /> : <IconInfoCircle size={17} />}>
|
||||
{allDocs
|
||||
? `All ${app.competencyDocs.length} competency certificates uploaded.`
|
||||
: `${app.competencyDocs.filter(d => !d.uploaded).length} competency certificate(s) missing.`}
|
||||
</Alert>
|
||||
|
||||
{/* Sea service */}
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<SectionLabel>Sea Service Record</SectionLabel>
|
||||
<Box maw={360}>
|
||||
<DocViewer fileName={app.seaServiceFile} label="Sea Service Record / Discharge Book" />
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Competency certs */}
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<SectionLabel>Competency Certificates ({app.competencyDocs.length} areas)</SectionLabel>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
||||
{app.competencyDocs.map((doc) => (
|
||||
<div key={doc.area}>
|
||||
<Group gap="xs" mb="xs">
|
||||
{doc.uploaded
|
||||
? <ThemeIcon size={18} radius="xl" color="teal" variant="filled"><IconCheck size={11} /></ThemeIcon>
|
||||
: <ThemeIcon size={18} radius="xl" color="red" variant="light"><IconX size={11} /></ThemeIcon>}
|
||||
<Text fz="xs" fw={700}>{doc.area}</Text>
|
||||
</Group>
|
||||
<DocViewer fileName={doc.filename} label={doc.area} />
|
||||
</div>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
|
||||
{/* ── TRB Inspection ────────────────────────────────────── */}
|
||||
<Tabs.Panel value="trb">
|
||||
<Stack gap="md">
|
||||
<Alert variant="light" color="yellow" icon={<IconInfoCircle size={17} />}>
|
||||
The Training Record Book (TRB) must be physically inspected at the EMA office.
|
||||
The applicant brings the original; record your inspection findings below after taking action.
|
||||
</Alert>
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<SectionLabel>Submitted TRB</SectionLabel>
|
||||
{app.trbFile
|
||||
? <Box maw={360}><DocViewer fileName={app.trbFile} label="Training Record Book (TRB)" /></Box>
|
||||
: <Badge color="gray" variant="light">Not submitted (entry-level certificate — TRB not required)</Badge>}
|
||||
</Paper>
|
||||
{app.trbInspectionNotes && (
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<SectionLabel>TRB Inspection Notes</SectionLabel>
|
||||
<Text fz="sm">{app.trbInspectionNotes}</Text>
|
||||
</Paper>
|
||||
)}
|
||||
{!app.trbInspectionNotes && app.trbFile && (
|
||||
<Alert variant="light" color="blue" icon={<IconBook2 size={17} />}>
|
||||
TRB inspection has not yet been recorded. Use "Take Action" above to approve, request resubmission, or reject the TRB.
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
|
||||
{/* ── Examination ───────────────────────────────────────── */}
|
||||
<Tabs.Panel value="exam">
|
||||
<Stack gap="md">
|
||||
{app.examDate ? (
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<SectionLabel>Scheduled Examination</SectionLabel>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md" mb="md">
|
||||
<InfoRow label="Examination Date" value={app.examDate} />
|
||||
<InfoRow label="Venue" value={app.examVenue ?? ''} />
|
||||
</SimpleGrid>
|
||||
{app.examNotes && (
|
||||
<>
|
||||
<Divider mb="md" />
|
||||
<SectionLabel>Examination Result Notes</SectionLabel>
|
||||
<Text fz="sm">{app.examNotes}</Text>
|
||||
</>
|
||||
)}
|
||||
{['Examination Scheduled'].includes(app.status) && (
|
||||
<Alert variant="light" color="indigo" icon={<IconCalendar size={17} />} mt="md">
|
||||
Examination is scheduled. After the examination, use "Take Action" to record the result (Pass or Fail).
|
||||
</Alert>
|
||||
)}
|
||||
</Paper>
|
||||
) : (
|
||||
<Alert variant="light" color="gray" icon={<IconInfoCircle size={17} />}>
|
||||
No examination scheduled yet. Examination is assigned by an EMA officer after TRB approval.
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
|
||||
{/* ── Payment ───────────────────────────────────────────── */}
|
||||
<Tabs.Panel value="payment">
|
||||
<Paper withBorder radius="lg" p="xl">
|
||||
<Group gap="sm" mb="lg">
|
||||
<ThemeIcon size="xl" variant="light" color="green" radius="lg"><IconCreditCard size={22} stroke={1.5} /></ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700} fz="lg">Payment Status</Text>
|
||||
<Badge size="sm" variant="light" color={app.paymentStatus === 'Verified' ? 'teal' : 'orange'} mt={2}>
|
||||
{app.paymentStatus}
|
||||
</Badge>
|
||||
</div>
|
||||
</Group>
|
||||
<Paper withBorder radius="md" p="md" bg="gray.0" mb="lg">
|
||||
<Text fw={700} fz="sm" mb="md">CoC / CoP Application Fee</Text>
|
||||
{[
|
||||
{ label: 'Application Processing Fee', amount: 800 },
|
||||
{ label: 'Document Verification Fee', amount: 300 },
|
||||
{ label: 'Examination Fee', amount: 500 },
|
||||
{ label: 'Certificate Issuance Fee', amount: 400 },
|
||||
].map(({ label, amount }) => (
|
||||
<Group key={label} justify="space-between" mb={4}>
|
||||
<Text fz="sm">{label}</Text>
|
||||
<Text fz="sm" fw={500}>ETB {amount.toFixed(2)}</Text>
|
||||
</Group>
|
||||
))}
|
||||
<Divider mt="sm" mb="xs" />
|
||||
<Group justify="space-between">
|
||||
<Text fz="sm" fw={800}>Total</Text>
|
||||
<Text fz="md" fw={800} c="blue">ETB 2,000.00</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<InfoRow label="Payment Method" value="CBE Bank Transfer" />
|
||||
<InfoRow label="Transaction Reference" value="CBE-TXN-20250315-042" />
|
||||
<InfoRow label="Payment Date" value={app.submitted} />
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
</Tabs.Panel>
|
||||
|
||||
{/* ── Issuance ──────────────────────────────────────────── */}
|
||||
<Tabs.Panel value="issuance">
|
||||
<Stack gap="md">
|
||||
<Alert
|
||||
variant="light"
|
||||
color={app.status === 'Certificate Issued' ? 'teal' : 'yellow'}
|
||||
icon={app.status === 'Certificate Issued' ? <IconCircleCheck size={17} /> : <IconInfoCircle size={17} />}
|
||||
>
|
||||
{app.status === 'Certificate Issued'
|
||||
? 'Certificate has been issued. The seafarer has been notified.'
|
||||
: 'Certificate will be issued after the application is fully approved and examination passed.'}
|
||||
</Alert>
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<SectionLabel>Certificate to be Issued</SectionLabel>
|
||||
<Card withBorder radius="lg" p="md" style={{ borderColor: app.status === 'Certificate Issued' ? 'var(--mantine-color-teal-3)' : 'var(--mantine-color-gray-2)' }}>
|
||||
<Group gap="sm" mb="sm" wrap="nowrap">
|
||||
<ThemeIcon size="xl" variant="light" color={app.status === 'Certificate Issued' ? 'teal' : 'gray'} radius="lg">
|
||||
<IconShieldCheck size={22} stroke={1.5} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700} fz="sm">{app.certType}</Text>
|
||||
<Text fz="xs" c="dimmed">{app.stcwRef}</Text>
|
||||
<Badge size="xs" variant="light" color={app.status === 'Certificate Issued' ? 'teal' : 'yellow'} mt={4}>
|
||||
{app.status === 'Certificate Issued' ? 'Issued' : 'Pending'}
|
||||
</Badge>
|
||||
</div>
|
||||
</Group>
|
||||
<Divider mb="sm" />
|
||||
<SimpleGrid cols={2} spacing="xs">
|
||||
<InfoRow label="Validity" value="5 years" />
|
||||
<InfoRow label="Revalidation" value="Before expiry — STCW Reg I/11" />
|
||||
</SimpleGrid>
|
||||
</Card>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
<ActionModal
|
||||
app={app}
|
||||
opened={actionModalOpen}
|
||||
onClose={() => setActionModalOpen(false)}
|
||||
onAction={handleAction}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user