mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
Copied verbatim from the pre-override branch so the approved screens are recoverable at this exact commit before any wiring changes them. Brings back the richer flows the client signed off: a four-step seafarer registration wizard with bilingual inputs and an Ethiopic date picker, the vessel-owner portal (its own register/login/dashboard), ownership transfer, and the seaman book, certificate, medical and endorsement screens. Six of these pages already call an API; ten are mockups carrying hardcoded data. Both are committed as-is here -- the wiring that follows is a separate commit so the diff shows exactly what changed from what the client approved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
596 lines
28 KiB
TypeScript
596 lines
28 KiB
TypeScript
import { useRef, useState } from 'react';
|
|
import {
|
|
Alert,
|
|
Badge,
|
|
Box,
|
|
Button,
|
|
Card,
|
|
Divider,
|
|
FileButton,
|
|
Group,
|
|
Paper,
|
|
SimpleGrid,
|
|
Stack,
|
|
Text,
|
|
TextInput,
|
|
Title,
|
|
rem,
|
|
} from '@mantine/core';
|
|
import {
|
|
IconAlertTriangle,
|
|
IconArrowLeft,
|
|
IconArrowRight,
|
|
IconBook2,
|
|
IconCheck,
|
|
IconCircleCheck,
|
|
IconCreditCard,
|
|
IconHeart,
|
|
IconId,
|
|
IconInfoCircle,
|
|
IconShieldCheck,
|
|
IconTrash,
|
|
IconUpload,
|
|
} from '@tabler/icons-react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { notify } from '@ema-platform/ui';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Steps
|
|
// ---------------------------------------------------------------------------
|
|
const STEPS = [
|
|
{ label: 'Relevant Certificate' },
|
|
{ label: 'Medical Certificate' },
|
|
{ label: 'Payment' },
|
|
{ label: 'Review & Submit' },
|
|
];
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Fee table — Seaman Book + BTC shown separately, paid together
|
|
// ---------------------------------------------------------------------------
|
|
const FEES = [
|
|
{ label: 'Seaman Book — Application Fee', amount: 500 },
|
|
{ label: 'Seaman Book — Document Verification Fee',amount: 200 },
|
|
{ label: 'Basic Training Certificate (BTC) — Application Fee', amount: 300 },
|
|
{ label: 'BTC — Document Verification Fee', amount: 100 },
|
|
{ label: 'BSID — Application Fee', amount: 100 },
|
|
{ label: 'BSID — Card Production Fee', amount: 150 },
|
|
];
|
|
const TOTAL = FEES.reduce((s, f) => s + f.amount, 0);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Step indicator
|
|
// ---------------------------------------------------------------------------
|
|
function StepIndicator({ active, completed }: { active: number; completed: number[] }) {
|
|
return (
|
|
<Box mb={32}>
|
|
<Group gap={0} align="center" wrap="nowrap">
|
|
{STEPS.map((step, i) => {
|
|
const isDone = completed.includes(i);
|
|
const isCurrent = active === i;
|
|
return (
|
|
<Group key={i} gap={0} align="center" style={{ flex: i < STEPS.length - 1 ? 1 : 'none' }}>
|
|
<Stack gap={4} align="center" style={{ minWidth: rem(40) }}>
|
|
<Box style={{
|
|
width: rem(40), height: rem(40), borderRadius: '50%',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
backgroundColor: isDone ? 'var(--mantine-color-blue-8)' : isCurrent ? 'var(--mantine-color-blue-7)' : 'var(--mantine-color-gray-1)',
|
|
border: isCurrent ? '2.5px solid var(--mantine-color-blue-5)' : '2px solid transparent',
|
|
boxShadow: isCurrent || isDone ? '0 2px 8px rgba(34,139,230,0.2)' : 'none',
|
|
flexShrink: 0, transition: 'all 0.2s ease',
|
|
}}>
|
|
{isDone ? <IconCheck size={18} color="white" stroke={2.5} /> : (
|
|
<Text fw={700} fz="sm" c={isCurrent ? 'white' : 'gray.5'}>{i + 1}</Text>
|
|
)}
|
|
</Box>
|
|
<Text fz="xs" fw={isCurrent ? 700 : 400} c={isCurrent ? 'blue.7' : 'dimmed'} style={{ whiteSpace: 'nowrap' }}>
|
|
{isDone ? `${step.label} ✓` : step.label}
|
|
</Text>
|
|
</Stack>
|
|
{i < STEPS.length - 1 && (
|
|
<Box style={{
|
|
flex: 1, height: rem(2),
|
|
backgroundColor: isDone ? 'var(--mantine-color-blue-8)' : 'var(--mantine-color-gray-2)',
|
|
marginBottom: rem(22),
|
|
}} />
|
|
)}
|
|
</Group>
|
|
);
|
|
})}
|
|
</Group>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function SectionHead({ title }: { title: string }) {
|
|
return (
|
|
<>
|
|
<Divider mt="md" mb="xs" />
|
|
<Text fw={600} fz="sm" tt="uppercase" c="gray.6">{title}</Text>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function ReviewRow({ 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>
|
|
);
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Main page
|
|
// ---------------------------------------------------------------------------
|
|
export function SeamanBookApplicationPage() {
|
|
const navigate = useNavigate();
|
|
const [active, setActive] = useState(0);
|
|
const [completed, setCompleted] = useState<number[]>([]);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
// Relevant Certificate
|
|
const [relCertNumber, setRelCertNumber] = useState('');
|
|
const [relIssuer, setRelIssuer] = useState('');
|
|
const [relIssueDate, setRelIssueDate] = useState('');
|
|
const [relExpiryDate, setRelExpiryDate] = useState('');
|
|
const [relFile, setRelFile] = useState<File | null>(null);
|
|
const relResetRef = useRef<() => void>(null);
|
|
|
|
// Medical
|
|
const [medCertNumber, setMedCertNumber] = useState('');
|
|
const [medIssuer, setMedIssuer] = useState('');
|
|
const [medIssueDate, setMedIssueDate] = useState('');
|
|
const [medExpiryDate, setMedExpiryDate] = useState('');
|
|
const [medFile, setMedFile] = useState<File | null>(null);
|
|
const medResetRef = useRef<() => void>(null);
|
|
|
|
// Payment
|
|
const [paymentMethod, setPaymentMethod] = useState<'cbe' | 'telebirr' | null>(null);
|
|
const [paymentRef, setPaymentRef] = useState('');
|
|
const [paymentDate, setPaymentDate] = useState('');
|
|
const [paymentFile, setPaymentFile] = useState<File | null>(null);
|
|
const payResetRef = useRef<() => void>(null);
|
|
|
|
// Validation
|
|
const relComplete = !!relFile && !!relCertNumber.trim() && !!relIssuer.trim() && !!relIssueDate && !!relExpiryDate;
|
|
const medComplete = !!medFile && !!medCertNumber.trim() && !!medIssuer.trim() && !!medIssueDate && !!medExpiryDate;
|
|
const payComplete = !!paymentMethod && !!paymentRef.trim() && !!paymentDate;
|
|
|
|
const canNext = () => {
|
|
if (active === 0) return relComplete;
|
|
if (active === 1) return medComplete;
|
|
if (active === 2) return payComplete;
|
|
return true;
|
|
};
|
|
|
|
const next = () => {
|
|
setCompleted((prev) => prev.includes(active) ? prev : [...prev, active]);
|
|
setActive((c) => c + 1);
|
|
};
|
|
const prev = () => setActive((c) => c - 1);
|
|
|
|
const handleSubmit = async () => {
|
|
setSubmitting(true);
|
|
try {
|
|
await new Promise((r) => setTimeout(r, 1400));
|
|
notify.success('Application submitted! Reference: SB-BTC-2025-001');
|
|
navigate('/seaman-book');
|
|
} catch {
|
|
notify.error('Submission failed. Please try again.');
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Stack gap="md">
|
|
<div>
|
|
<Title order={3}>Seaman Book, BTC & BSID Application</Title>
|
|
<Text fz="sm" c="dimmed">
|
|
One application covers your Seaman Book, Basic Training Certificate (BTC), and BSID — Step {active + 1} of {STEPS.length}
|
|
</Text>
|
|
</div>
|
|
|
|
{/* What you will receive banner */}
|
|
<Paper withBorder radius="md" p="md" bg="blue.0" style={{ borderColor: 'var(--mantine-color-blue-2)' }}>
|
|
<Group gap="lg" wrap="wrap">
|
|
<Group gap="xs">
|
|
<IconBook2 size={18} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
|
<Text fz="sm" fw={600} c="blue.8">Seaman Book</Text>
|
|
</Group>
|
|
<Text fz="sm" c="dimmed">+</Text>
|
|
<Group gap="xs">
|
|
<IconShieldCheck size={18} color="var(--mantine-color-teal-6)" stroke={1.5} />
|
|
<Text fz="sm" fw={600} c="teal.8">Basic Training Certificate (BTC)</Text>
|
|
</Group>
|
|
<Text fz="sm" c="dimmed">+</Text>
|
|
<Group gap="xs">
|
|
<IconId size={18} color="var(--mantine-color-violet-6)" stroke={1.5} />
|
|
<Text fz="sm" fw={600} c="violet.8">BSID (Biometric Seafarer ID)</Text>
|
|
</Group>
|
|
</Group>
|
|
</Paper>
|
|
|
|
<StepIndicator active={active} completed={completed} />
|
|
|
|
<Paper withBorder radius="lg" p="xl">
|
|
<Group justify="space-between" mb="lg">
|
|
<Text fw={700} fz="lg">{STEPS[active]?.label}</Text>
|
|
<Badge variant="light" color="blue" radius="md">Step {active + 1} of {STEPS.length}</Badge>
|
|
</Group>
|
|
|
|
{/* ── Step 1: Relevant Certificate ────────────────────────────── */}
|
|
{active === 0 && (
|
|
<Stack gap="md">
|
|
<Alert variant="light" color="blue" icon={<IconInfoCircle size={17} />}>
|
|
Upload your relevant certificate issued by an EMA-approved training institution. This is the prerequisite for your Basic Training Certificate (BTC).
|
|
</Alert>
|
|
<SectionHead title="Relevant Certificate Details" />
|
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
|
<TextInput
|
|
label="Certificate Number"
|
|
placeholder="e.g. CERT-2024-001"
|
|
required
|
|
value={relCertNumber}
|
|
onChange={(e) => setRelCertNumber(e.currentTarget.value)}
|
|
/>
|
|
<TextInput
|
|
label="Issuing Institution"
|
|
placeholder="e.g. Bahirdar Maritime School"
|
|
required
|
|
value={relIssuer}
|
|
onChange={(e) => setRelIssuer(e.currentTarget.value)}
|
|
/>
|
|
</SimpleGrid>
|
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
|
<TextInput
|
|
label="Issue Date"
|
|
type="date"
|
|
required
|
|
value={relIssueDate}
|
|
onChange={(e) => setRelIssueDate(e.currentTarget.value)}
|
|
/>
|
|
<TextInput
|
|
label="Expiry Date"
|
|
type="date"
|
|
required
|
|
value={relExpiryDate}
|
|
onChange={(e) => setRelExpiryDate(e.currentTarget.value)}
|
|
/>
|
|
</SimpleGrid>
|
|
<SectionHead title="Upload Certificate" />
|
|
<Card
|
|
withBorder
|
|
radius="md"
|
|
p="md"
|
|
style={{
|
|
borderStyle: relFile ? 'solid' : 'dashed',
|
|
borderColor: relFile ? 'var(--mantine-color-teal-5)' : 'var(--mantine-color-default-border)',
|
|
maxWidth: rem(420),
|
|
}}
|
|
>
|
|
<Group gap="sm" mb="sm" wrap="nowrap">
|
|
<Box style={{
|
|
width: rem(40), height: rem(40), borderRadius: rem(8),
|
|
background: relFile ? 'var(--mantine-color-teal-light)' : 'var(--mantine-color-blue-light)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
|
}}>
|
|
<IconShieldCheck size={20} color={relFile ? 'var(--mantine-color-teal-6)' : 'var(--mantine-color-blue-6)'} stroke={1.5} />
|
|
</Box>
|
|
<div>
|
|
<Text fw={600} fz="sm">Relevant Certificate <Text span c="red">*</Text></Text>
|
|
<Text fz="xs" c="dimmed">PDF, JPG or PNG — max 5MB</Text>
|
|
</div>
|
|
</Group>
|
|
{relFile ? (
|
|
<Group gap="xs">
|
|
<IconCircleCheck size={15} color="var(--mantine-color-teal-6)" />
|
|
<Text fz="xs" c="teal.7" style={{ flex: 1 }} truncate>{relFile.name}</Text>
|
|
<Button size="xs" variant="subtle" color="red" onClick={() => { setRelFile(null); relResetRef.current?.(); }}>
|
|
<IconTrash size={13} />
|
|
</Button>
|
|
</Group>
|
|
) : (
|
|
<FileButton resetRef={relResetRef} onChange={setRelFile} accept="application/pdf,image/jpeg,image/png">
|
|
{(props) => (
|
|
<Button size="sm" variant="default" leftSection={<IconUpload size={14} />} fullWidth {...props}>
|
|
Choose File
|
|
</Button>
|
|
)}
|
|
</FileButton>
|
|
)}
|
|
</Card>
|
|
</Stack>
|
|
)}
|
|
|
|
{/* ── Step 2: Medical ─────────────────────────────────────────── */}
|
|
{active === 1 && (
|
|
<Stack gap="md">
|
|
<Alert variant="light" color="blue" icon={<IconInfoCircle size={17} />}>
|
|
Upload your valid medical certificate from an EMA-approved medical centre. Required for both Seaman Book and BTC issuance.
|
|
</Alert>
|
|
<SectionHead title="Medical Certificate Details" />
|
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
|
<TextInput label="Certificate Number" placeholder="e.g. MC-2024-001" required value={medCertNumber} onChange={(e) => setMedCertNumber(e.currentTarget.value)} />
|
|
<TextInput label="Issuing Medical Centre" placeholder="e.g. EMA Medical Centre" required value={medIssuer} onChange={(e) => setMedIssuer(e.currentTarget.value)} />
|
|
</SimpleGrid>
|
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
|
<TextInput label="Issue Date" type="date" required value={medIssueDate} onChange={(e) => setMedIssueDate(e.currentTarget.value)} />
|
|
<TextInput label="Expiry Date" type="date" required value={medExpiryDate} onChange={(e) => setMedExpiryDate(e.currentTarget.value)} />
|
|
</SimpleGrid>
|
|
<SectionHead title="Upload Certificate" />
|
|
<Card withBorder radius="md" p="md" style={{
|
|
borderStyle: medFile ? 'solid' : 'dashed',
|
|
borderColor: medFile ? 'var(--mantine-color-teal-5)' : 'var(--mantine-color-default-border)',
|
|
maxWidth: rem(420),
|
|
}}>
|
|
<Group gap="sm" mb="sm" wrap="nowrap">
|
|
<Box style={{
|
|
width: rem(40), height: rem(40), borderRadius: rem(8),
|
|
background: medFile ? 'var(--mantine-color-teal-light)' : 'var(--mantine-color-blue-light)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
|
}}>
|
|
<IconHeart size={20} color={medFile ? 'var(--mantine-color-teal-6)' : 'var(--mantine-color-blue-6)'} stroke={1.5} />
|
|
</Box>
|
|
<div>
|
|
<Text fw={600} fz="sm">Medical Certificate <Text span c="red">*</Text></Text>
|
|
<Text fz="xs" c="dimmed">PDF, JPG or PNG — max 5MB</Text>
|
|
</div>
|
|
</Group>
|
|
{medFile ? (
|
|
<Group gap="xs">
|
|
<IconCircleCheck size={15} color="var(--mantine-color-teal-6)" />
|
|
<Text fz="xs" c="teal.7" style={{ flex: 1 }} truncate>{medFile.name}</Text>
|
|
<Button size="xs" variant="subtle" color="red" onClick={() => { setMedFile(null); medResetRef.current?.(); }}>
|
|
<IconTrash size={13} />
|
|
</Button>
|
|
</Group>
|
|
) : (
|
|
<FileButton resetRef={medResetRef} onChange={setMedFile} accept="application/pdf,image/jpeg,image/png">
|
|
{(props) => (
|
|
<Button size="sm" variant="default" leftSection={<IconUpload size={14} />} fullWidth {...props}>
|
|
Choose File
|
|
</Button>
|
|
)}
|
|
</FileButton>
|
|
)}
|
|
</Card>
|
|
<Alert variant="light" color="yellow" icon={<IconAlertTriangle size={17} />}>
|
|
Only certificates from <strong>EMA-approved medical centres</strong> are accepted.
|
|
</Alert>
|
|
</Stack>
|
|
)}
|
|
|
|
{/* ── Step 3: Payment ─────────────────────────────────────────── */}
|
|
{active === 2 && (
|
|
<Stack gap="md">
|
|
{/* Fee breakdown — SB + BTC shown separately */}
|
|
<Paper withBorder radius="md" p="md" bg="gray.0">
|
|
<Text fw={700} fz="sm" mb="xs">Fee Breakdown</Text>
|
|
<Text fz="xs" c="dimmed" mb="md">Your payment covers both the Seaman Book and Basic Training Certificate (BTC).</Text>
|
|
|
|
{/* SB fees */}
|
|
<Text fz="xs" fw={700} tt="uppercase" c="blue.7" mb={6}>Seaman Book</Text>
|
|
{FEES.filter(f => f.label.startsWith('Seaman Book')).map(({ label, amount }) => (
|
|
<Group key={label} justify="space-between" mb={4}>
|
|
<Text fz="sm">{label.replace('Seaman Book — ', '')}</Text>
|
|
<Text fz="sm" fw={500}>ETB {amount.toFixed(2)}</Text>
|
|
</Group>
|
|
))}
|
|
|
|
<Divider my="xs" />
|
|
|
|
{/* BTC fees */}
|
|
<Text fz="xs" fw={700} tt="uppercase" c="teal.7" mb={6}>Basic Training Certificate (BTC)</Text>
|
|
{FEES.filter(f => f.label.startsWith('Basic Training') || f.label.startsWith('BTC')).map(({ label, amount }) => (
|
|
<Group key={label} justify="space-between" mb={4}>
|
|
<Text fz="sm">{label.replace('Basic Training Certificate (BTC) — ', '').replace('BTC — ', '')}</Text>
|
|
<Text fz="sm" fw={500}>ETB {amount.toFixed(2)}</Text>
|
|
</Group>
|
|
))}
|
|
|
|
<Divider my="xs" />
|
|
|
|
{/* BSID fees */}
|
|
<Divider my="xs" />
|
|
<Text fz="xs" fw={700} tt="uppercase" c="violet.7" mb={6}>BSID (Biometric Seafarer ID)</Text>
|
|
{FEES.filter(f => f.label.startsWith('BSID')).map(({ label, amount }) => (
|
|
<Group key={label} justify="space-between" mb={4}>
|
|
<Text fz="sm">{label.replace('BSID — ', '')}</Text>
|
|
<Text fz="sm" fw={500}>ETB {amount.toFixed(2)}</Text>
|
|
</Group>
|
|
))}
|
|
|
|
<Divider mt="xs" mb="sm" />
|
|
<Group justify="space-between">
|
|
<Text fz="sm" fw={800}>Total Amount Due</Text>
|
|
<Text fz="md" fw={800} c="blue">ETB {TOTAL.toFixed(2)}</Text>
|
|
</Group>
|
|
</Paper>
|
|
|
|
<SectionHead title="Select Payment Method" />
|
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md" style={{ maxWidth: rem(600) }}>
|
|
{/* CBE */}
|
|
<Card withBorder radius="md" p="md" onClick={() => { setPaymentMethod('cbe'); setPaymentRef(''); }}
|
|
style={{
|
|
cursor: 'pointer',
|
|
borderColor: paymentMethod === 'cbe' ? 'var(--mantine-color-blue-6)' : 'var(--mantine-color-default-border)',
|
|
borderWidth: paymentMethod === 'cbe' ? 2 : 1,
|
|
background: paymentMethod === 'cbe' ? 'var(--mantine-color-blue-light)' : undefined,
|
|
}}>
|
|
<Group gap="sm" wrap="nowrap">
|
|
<Box style={{
|
|
width: rem(44), height: rem(44), borderRadius: rem(8), flexShrink: 0,
|
|
background: 'var(--mantine-color-blue-light)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
}}>
|
|
<IconCreditCard size={22} color="var(--mantine-color-blue-6)" stroke={1.5} />
|
|
</Box>
|
|
<div>
|
|
<Text fw={700} fz="sm">CBE Bank Transfer</Text>
|
|
<Text fz="xs" c="dimmed">Commercial Bank of Ethiopia</Text>
|
|
</div>
|
|
{paymentMethod === 'cbe' && <IconCircleCheck size={18} color="var(--mantine-color-blue-6)" style={{ marginLeft: 'auto' }} />}
|
|
</Group>
|
|
</Card>
|
|
|
|
{/* Telebirr */}
|
|
<Card withBorder radius="md" p="md" onClick={() => { setPaymentMethod('telebirr'); setPaymentRef(''); }}
|
|
style={{
|
|
cursor: 'pointer',
|
|
borderColor: paymentMethod === 'telebirr' ? 'var(--mantine-color-violet-6)' : 'var(--mantine-color-default-border)',
|
|
borderWidth: paymentMethod === 'telebirr' ? 2 : 1,
|
|
background: paymentMethod === 'telebirr' ? 'var(--mantine-color-violet-light)' : undefined,
|
|
}}>
|
|
<Group gap="sm" wrap="nowrap">
|
|
<Box style={{
|
|
width: rem(44), height: rem(44), borderRadius: rem(8), flexShrink: 0,
|
|
background: 'var(--mantine-color-violet-light)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
}}>
|
|
<IconCreditCard size={22} color="var(--mantine-color-violet-6)" stroke={1.5} />
|
|
</Box>
|
|
<div>
|
|
<Text fw={700} fz="sm">Telebirr</Text>
|
|
<Text fz="xs" c="dimmed">Ethio Telecom Mobile Money</Text>
|
|
</div>
|
|
{paymentMethod === 'telebirr' && <IconCircleCheck size={18} color="var(--mantine-color-violet-6)" style={{ marginLeft: 'auto' }} />}
|
|
</Group>
|
|
</Card>
|
|
</SimpleGrid>
|
|
|
|
{paymentMethod === 'cbe' && (
|
|
<>
|
|
<Alert variant="light" color="blue" icon={<IconInfoCircle size={15} />}>
|
|
Transfer <strong>ETB {TOTAL.toFixed(2)}</strong> to CBE Account <strong>1000123456789</strong> (EMA Maritime Authority). Use your full name as description.
|
|
</Alert>
|
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
|
<TextInput label="CBE Transaction Reference" placeholder="e.g. CBE-TXN-20240510-001" required value={paymentRef} onChange={(e) => setPaymentRef(e.currentTarget.value)} />
|
|
<TextInput label="Payment Date" type="date" required value={paymentDate} onChange={(e) => setPaymentDate(e.currentTarget.value)} />
|
|
</SimpleGrid>
|
|
</>
|
|
)}
|
|
|
|
{paymentMethod === 'telebirr' && (
|
|
<>
|
|
<Alert variant="light" color="violet" icon={<IconInfoCircle size={15} />}>
|
|
Send <strong>ETB {TOTAL.toFixed(2)}</strong> to Telebirr <strong>+251 11 551 0000</strong> (EMA Maritime Authority). Screenshot and upload your confirmation.
|
|
</Alert>
|
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
|
<TextInput label="Telebirr Transaction ID" placeholder="e.g. TLB-2024-001234" required value={paymentRef} onChange={(e) => setPaymentRef(e.currentTarget.value)} />
|
|
<TextInput label="Payment Date" type="date" required value={paymentDate} onChange={(e) => setPaymentDate(e.currentTarget.value)} />
|
|
</SimpleGrid>
|
|
</>
|
|
)}
|
|
|
|
{paymentMethod && (
|
|
<>
|
|
<SectionHead title="Upload Receipt (optional)" />
|
|
<Card withBorder radius="md" p="md" style={{
|
|
borderStyle: paymentFile ? 'solid' : 'dashed',
|
|
borderColor: paymentFile ? 'var(--mantine-color-teal-5)' : 'var(--mantine-color-default-border)',
|
|
maxWidth: rem(420),
|
|
}}>
|
|
<Group gap="sm" mb="sm" wrap="nowrap">
|
|
<Box style={{
|
|
width: rem(40), height: rem(40), borderRadius: rem(8),
|
|
background: paymentFile ? 'var(--mantine-color-teal-light)' : 'var(--mantine-color-blue-light)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
|
}}>
|
|
<IconCreditCard size={20} color={paymentFile ? 'var(--mantine-color-teal-6)' : 'var(--mantine-color-blue-6)'} stroke={1.5} />
|
|
</Box>
|
|
<div>
|
|
<Text fw={600} fz="sm">{paymentMethod === 'telebirr' ? 'Telebirr Screenshot' : 'Bank Receipt'}</Text>
|
|
<Text fz="xs" c="dimmed">PDF, JPG or PNG — max 5MB</Text>
|
|
</div>
|
|
</Group>
|
|
{paymentFile ? (
|
|
<Group gap="xs">
|
|
<IconCircleCheck size={15} color="var(--mantine-color-teal-6)" />
|
|
<Text fz="xs" c="teal.7" style={{ flex: 1 }} truncate>{paymentFile.name}</Text>
|
|
<Button size="xs" variant="subtle" color="red" onClick={() => { setPaymentFile(null); payResetRef.current?.(); }}>
|
|
<IconTrash size={13} />
|
|
</Button>
|
|
</Group>
|
|
) : (
|
|
<FileButton resetRef={payResetRef} onChange={setPaymentFile} accept="application/pdf,image/jpeg,image/png">
|
|
{(props) => (
|
|
<Button size="sm" variant="default" leftSection={<IconUpload size={14} />} fullWidth {...props}>
|
|
Upload Receipt
|
|
</Button>
|
|
)}
|
|
</FileButton>
|
|
)}
|
|
</Card>
|
|
</>
|
|
)}
|
|
</Stack>
|
|
)}
|
|
|
|
{/* ── Step 4: Review ──────────────────────────────────────────── */}
|
|
{active === 3 && (
|
|
<Stack gap="md">
|
|
<Alert variant="light" color="blue" icon={<IconInfoCircle size={17} />}>
|
|
Submitting this application will initiate processing for both your <strong>Seaman Book</strong> and <strong>Basic Training Certificate (BTC)</strong>.
|
|
</Alert>
|
|
|
|
<Paper withBorder radius="md" p="md">
|
|
<Text fw={700} fz="sm" mb="sm">Relevant Certificate</Text>
|
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
|
<ReviewRow label="Certificate No." value={relCertNumber} />
|
|
<ReviewRow label="Issuing Institution" value={relIssuer} />
|
|
<ReviewRow label="Issue Date" value={relIssueDate} />
|
|
<ReviewRow label="Expiry Date" value={relExpiryDate} />
|
|
<ReviewRow label="Document" value={relFile?.name ?? '—'} />
|
|
</SimpleGrid>
|
|
</Paper>
|
|
|
|
<Paper withBorder radius="md" p="md">
|
|
<Text fw={700} fz="sm" mb="sm">Medical Certificate</Text>
|
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
|
<ReviewRow label="Certificate No." value={medCertNumber} />
|
|
<ReviewRow label="Issuing Centre" value={medIssuer} />
|
|
<ReviewRow label="Issue Date" value={medIssueDate} />
|
|
<ReviewRow label="Expiry Date" value={medExpiryDate} />
|
|
<ReviewRow label="Document" value={medFile?.name ?? '—'} />
|
|
</SimpleGrid>
|
|
</Paper>
|
|
|
|
<Paper withBorder radius="md" p="md">
|
|
<Text fw={700} fz="sm" mb="sm">Payment</Text>
|
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
|
<ReviewRow label="Payment Method" value={paymentMethod === 'cbe' ? 'CBE Bank Transfer' : 'Telebirr'} />
|
|
<ReviewRow label="Transaction Reference" value={paymentRef} />
|
|
<ReviewRow label="Payment Date" value={paymentDate} />
|
|
<ReviewRow label="Total Paid" value={`ETB ${TOTAL.toFixed(2)}`} />
|
|
<ReviewRow label="Receipt" value={paymentFile?.name ?? 'Not uploaded'} />
|
|
</SimpleGrid>
|
|
</Paper>
|
|
</Stack>
|
|
)}
|
|
|
|
{/* Navigation */}
|
|
<Group justify="space-between" mt="xl">
|
|
<Button variant="default" onClick={() => navigate('/seaman-book')}>Cancel</Button>
|
|
<Group gap="sm">
|
|
{active > 0 && (
|
|
<Button variant="default" leftSection={<IconArrowLeft size={16} />} onClick={prev}>Previous</Button>
|
|
)}
|
|
{active < STEPS.length - 1 ? (
|
|
<Button rightSection={<IconArrowRight size={16} />} onClick={next} disabled={!canNext()}>
|
|
Next Step
|
|
</Button>
|
|
) : (
|
|
<Button color="blue" leftSection={<IconBook2 size={16} />} onClick={handleSubmit} loading={submitting}>
|
|
Submit Application
|
|
</Button>
|
|
)}
|
|
</Group>
|
|
</Group>
|
|
</Paper>
|
|
</Stack>
|
|
);
|
|
}
|