Files
emaui/apps/portal/src/app/features/shipping-agent-license/pages/ShippingAgentLicenseRenewalPage.tsx
fitse-yotor e39f80941e feat: add Shipping Agent License Renewal and Waiver Application pages
- Implement ShippingAgentLicenseRenewalPage for submitting renewal documents with file uploads.
- Create WaiverApplicationPage for multi-step waiver application process, including document uploads and review.
- Add WaiverPage to display application status and details, including waiver letter download functionality.
- Introduce document handling components and validation for required fields in both applications.
2026-07-28 14:46:06 +03:00

147 lines
5.6 KiB
TypeScript

import { useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useApiMutation } from '@ema-platform/api';
import {
Alert,
Box,
Button,
Card,
Group,
Paper,
Stack,
Text,
ThemeIcon,
Title,
rem,
} from '@mantine/core';
import {
IconArrowLeft,
IconCheck,
IconCircleCheck,
IconFileDescription,
IconInfoCircle,
IconShip,
} from '@tabler/icons-react';
import { FileButton } from '@mantine/core';
import { notify } from '@ema-platform/ui';
interface DocSlot {
key: string;
label: string;
description: string;
required: boolean;
}
const RENEWAL_DOCS: DocSlot[] = [
{ key: 'prevCertificate', label: 'Previous Shipping Agent Certificate', description: 'Your current/expiring certificate', required: true },
{ key: 'taxClearance', label: 'Tax Clearance', description: 'Current tax clearance certificate', required: true },
{ key: 'threeMonthClearance', label: 'Three-Month Clearance Evidence', description: 'Clearance evidence for the last three months', required: true },
{ key: 'vehicleRenewal', label: 'Updated Vehicle Rental Agreement', description: 'Required only if the previous agreement has expired', required: false },
{ key: 'officeRenewal', label: 'Updated Office Rental Agreement', description: 'Required only if the previous agreement has expired', required: false },
{ key: 'paymentReceipt', label: 'Renewal Service Payment Receipt (600 ETB)', description: 'Proof of payment for the renewal service fee', required: true },
{ key: 'renewalDeclaration', label: 'Applicant Renewal Declaration', description: 'Signed declaration confirming renewal details', required: true },
];
function DocCard({ slot, file, onFile }: { slot: DocSlot; file: File | null; onFile: (f: File | null) => void }) {
const resetRef = useRef<() => void>(null);
return (
<Card withBorder radius="md" p="md" style={{
borderStyle: 'dashed',
borderColor: file ? 'var(--mantine-color-teal-5)' : 'var(--mantine-color-default-border)',
}}>
<Group gap="sm" mb="sm" wrap="nowrap">
<Box style={{
width: rem(44), height: rem(44), borderRadius: rem(8),
background: 'var(--mantine-color-blue-light)', display: 'flex',
alignItems: 'center', justifyContent: 'center', flexShrink: 0,
}}>
<IconFileDescription size={22} color="var(--mantine-color-blue-6)" stroke={1.5} />
</Box>
<div>
<Text fw={600} fz="sm">{slot.label}{slot.required && <Text span c="red" ml={3}>*</Text>}</Text>
<Text fz="xs" c="dimmed">{slot.description}</Text>
</div>
</Group>
{file ? (
<Group gap="xs" align="center">
<IconCircleCheck size={16} color="var(--mantine-color-teal-6)" />
<Text fz="xs" c="teal.7" style={{ flex: 1 }} truncate>{file.name}</Text>
<Button size="xs" variant="subtle" color="red" onClick={() => { onFile(null); resetRef.current?.(); }}>Remove</Button>
</Group>
) : (
<FileButton resetRef={resetRef} onChange={onFile} accept="application/pdf,image/jpeg,image/png">
{(props) => <Button size="xs" variant="default" {...props}>Choose File</Button>}
</FileButton>
)}
</Card>
);
}
export function ShippingAgentLicenseRenewalPage() {
const navigate = useNavigate();
const [submitTrigger] = useApiMutation<{ id: string }>();
const [submitting, setSubmitting] = useState(false);
const [files, setFiles] = useState<Record<string, File | null>>(
Object.fromEntries(RENEWAL_DOCS.map((s) => [s.key, null]))
);
const setFile = (key: string) => (f: File | null) => setFiles((prev) => ({ ...prev, [key]: f }));
const canSubmit = RENEWAL_DOCS.every((s) => !s.required || !!files[s.key]);
const handleSubmit = async () => {
setSubmitting(true);
try {
await submitTrigger({ url: '/logistics-licenses/shipping-agent/renew', method: 'POST', body: {} }).unwrap();
notify.success('Renewal request submitted successfully!');
navigate('/shipping-agent-license');
} catch {
notify.error('Renewal submission failed. Please try again.');
} finally {
setSubmitting(false);
}
};
return (
<Stack gap="md">
<Group gap="xs">
<Button variant="subtle" size="xs" leftSection={<IconArrowLeft size={14} />} onClick={() => navigate('/shipping-agent-license')}>
Back
</Button>
</Group>
<Group gap="sm">
<ThemeIcon size={44} radius="md" color="blue" variant="light"><IconShip size={24} /></ThemeIcon>
<div>
<Title order={3}>Renew Shipping Agent License</Title>
<Text fz="sm" c="dimmed">Submit renewal documents to extend your license by one year</Text>
</div>
</Group>
<Alert variant="light" color="blue" icon={<IconInfoCircle size={16} />}>
A renewal service payment of 600 ETB is required. If your vehicle or office rental agreement has expired since your last submission, an updated agreement is required.
</Alert>
<Paper withBorder radius="lg" p="xl">
<Text fw={700} fz="lg" mb="lg">Renewal Documents</Text>
<Stack gap="md">
{RENEWAL_DOCS.map((slot) => (
<DocCard key={slot.key} slot={slot} file={files[slot.key]} onFile={setFile(slot.key)} />
))}
</Stack>
<Group justify="flex-end" mt="xl">
<Button
color="teal"
leftSection={<IconCheck size={16} />}
loading={submitting}
disabled={!canSubmit}
onClick={handleSubmit}
>
Submit Renewal Request
</Button>
</Group>
</Paper>
</Stack>
);
}