mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-08 04:15:44 +00:00
Adding license generation functionality
This commit is contained in:
@@ -1,446 +1,21 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Group,
|
||||
Modal,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
Textarea,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
rem,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconArrowLeft,
|
||||
IconBook2,
|
||||
IconCertificate,
|
||||
IconCheck,
|
||||
IconCircleCheck,
|
||||
IconCreditCard,
|
||||
IconExternalLink,
|
||||
IconEye,
|
||||
IconFileDescription,
|
||||
IconHeart,
|
||||
IconInfoCircle,
|
||||
IconShieldCheck,
|
||||
IconUser,
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { Container } from '@mantine/core';
|
||||
import { FeatureUnavailable } from '@ema-platform/ui';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock data
|
||||
// ---------------------------------------------------------------------------
|
||||
const FEES = [
|
||||
{ group: 'Seaman Book', label: 'Application Fee', amount: 500 },
|
||||
{ group: 'Seaman Book', label: 'Document Verification Fee', amount: 200 },
|
||||
{ group: 'Basic Training Certificate (BTC)', label: 'Application Fee', amount: 300 },
|
||||
{ group: 'Basic Training Certificate (BTC)', label: 'Document Verification Fee',amount: 100 },
|
||||
{ group: 'BSID', label: 'Application Fee', amount: 100 },
|
||||
];
|
||||
const TOTAL = FEES.reduce((s, f) => s + f.amount, 0);
|
||||
|
||||
const MOCK_APPLICATION = {
|
||||
id: 'SB-APP-2025-001',
|
||||
submittedAt: '2025-05-10',
|
||||
status: 'pending_review' as const,
|
||||
applicant: {
|
||||
fullName: 'Abebe Girma Tadesse',
|
||||
dob: '1990-04-15',
|
||||
nationality: 'Ethiopian',
|
||||
idNumber: 'ID-ETH-2024-001',
|
||||
phone: '+251 91 234 5678',
|
||||
email: 'abebe.girma@email.com',
|
||||
address: 'Bole, Addis Ababa, Ethiopia',
|
||||
height: '178 cm',
|
||||
bloodType: 'O+',
|
||||
hairColor: 'Black',
|
||||
eyeColor: 'Dark Brown',
|
||||
seafarerNumber: 'SEAF-2024-00142',
|
||||
},
|
||||
bst: [
|
||||
{ key: 'pst', short: 'PST', label: 'Personal Survival Techniques', certNumber: 'PST-2024-001', issuer: 'Bahirdar Maritime Training School', issueDate: '2024-01-15', expiryDate: '2029-01-15', fileName: 'pst_cert.pdf', fileType: 'pdf' },
|
||||
{ key: 'fpff', short: 'FPFF', label: 'Fire Prevention & Fire Fighting', certNumber: 'FPFF-2024-002', issuer: 'Bahirdar Maritime Training School', issueDate: '2024-01-16', expiryDate: '2029-01-16', fileName: 'fpff_cert.pdf', fileType: 'pdf' },
|
||||
{ key: 'efa', short: 'EFA', label: 'Elementary First Aid', certNumber: 'EFA-2024-003', issuer: 'EMA Training Centre', issueDate: '2024-02-01', expiryDate: '', fileName: 'efa_cert.jpg', fileType: 'image' },
|
||||
{ key: 'pssr', short: 'PSSR', label: 'Personal Safety & Social Responsibility', certNumber: 'PSSR-2024-004', issuer: 'EMA Training Centre', issueDate: '2024-02-02', expiryDate: '', fileName: 'pssr_cert.jpg', fileType: 'image' },
|
||||
{ key: 'shpt', short: 'SHPT', label: 'Sexual Harassment Prevention Training', certNumber: 'SHPT-2024-005', issuer: 'EMA Training Centre', issueDate: '2024-02-03', expiryDate: '', fileName: 'shpt_cert.jpg', fileType: 'image' },
|
||||
],
|
||||
medical: {
|
||||
certNumber: 'MC-2024-009',
|
||||
issuer: 'EMA Medical Centre — Addis Ababa',
|
||||
issueDate: '2024-03-20',
|
||||
expiryDate: '2026-03-20',
|
||||
fileName: 'medical_cert.pdf',
|
||||
fileType: 'pdf',
|
||||
},
|
||||
payment: {
|
||||
method: 'CBE Bank Transfer',
|
||||
ref: 'CBE-TXN-20240510-001',
|
||||
date: '2025-05-10',
|
||||
status: 'confirmed' as 'confirmed' | 'pending' | 'rejected',
|
||||
receiptFileName: 'payment_receipt.pdf',
|
||||
receiptFileType: 'pdf',
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
pending_review: 'yellow', approved: 'teal', correction_required: 'orange', rejected: 'red',
|
||||
};
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
pending_review: 'Pending Review', approved: 'Approved', correction_required: 'Correction Required', rejected: 'Rejected',
|
||||
};
|
||||
const PAYMENT_COLOR = { confirmed: 'teal', pending: 'yellow', rejected: 'red' } as const;
|
||||
|
||||
const DEMO_PDF = 'data:application/pdf;base64,JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PgplbmRvYmoKMyAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDIgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagp4cmVmCjAgNAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA1OCAwMDAwMCBuIAowMDAwMDAwMTE1IDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSA0L1Jvb3QgMSAwIFI+PgpzdGFydHhyZWYKMjIwCiUlRU9G';
|
||||
|
||||
function getDocUrl(fileName: string) {
|
||||
return fileName.endsWith('.pdf') ? DEMO_PDF : `https://placehold.co/600x400/e9ecef/6c757d?text=${encodeURIComponent(fileName)}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Inline document viewer
|
||||
// ---------------------------------------------------------------------------
|
||||
function DocViewer({ fileName, fileType, label }: { fileName: string; fileType: string; label: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const url = getDocUrl(fileName);
|
||||
return (
|
||||
<>
|
||||
<Card withBorder radius="md" p="sm">
|
||||
<Group gap="sm" wrap="nowrap" mb="sm">
|
||||
<ThemeIcon size="md" variant="light" color={fileType === 'pdf' ? '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={fileType === 'pdf' ? 'red' : 'blue'}>{fileType.toUpperCase()}</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)' }}>
|
||||
{fileType === 'pdf'
|
||||
? <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' } }}>
|
||||
{fileType === 'pdf'
|
||||
? <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>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main page
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* Placeholder until this feature has a backend.
|
||||
*
|
||||
* This page previously rendered hardcoded sample records, which were
|
||||
* indistinguishable from real ones.
|
||||
*/
|
||||
export function ApplicationReviewPage() {
|
||||
const navigate = useNavigate();
|
||||
const app = MOCK_APPLICATION;
|
||||
|
||||
const [status, setStatus] = useState<string>(app.status);
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
||||
const [noteModalOpen, setNoteModalOpen] = useState(false);
|
||||
const [noteAction, setNoteAction] = useState<'correction' | 'reject' | null>(null);
|
||||
const [note, setNote] = useState('');
|
||||
|
||||
const performAction = async (action: string, label: string, newStatus: string) => {
|
||||
setActionLoading(action);
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
setStatus(newStatus);
|
||||
setActionLoading(null);
|
||||
notify.success(`Application ${label}`);
|
||||
if (noteModalOpen) setNoteModalOpen(false);
|
||||
};
|
||||
|
||||
const openNote = (action: 'correction' | 'reject') => {
|
||||
setNoteAction(action);
|
||||
setNote('');
|
||||
setNoteModalOpen(true);
|
||||
};
|
||||
|
||||
// Group fees by category for display
|
||||
const feeGroups = FEES.reduce<Record<string, typeof FEES>>((acc, f) => {
|
||||
(acc[f.group] = acc[f.group] || []).push(f);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Header */}
|
||||
<Group justify="space-between">
|
||||
<Group gap="sm">
|
||||
<ActionIcon variant="subtle" size="lg" onClick={() => navigate('/seaman-book-queue')}>
|
||||
<IconArrowLeft size={18} />
|
||||
</ActionIcon>
|
||||
<div>
|
||||
<Title order={3}>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.submittedAt}</Text>
|
||||
</Group>
|
||||
</div>
|
||||
</Group>
|
||||
<Badge size="lg" variant="light" color={STATUS_COLOR[status] ?? 'gray'}>
|
||||
{STATUS_LABEL[status] ?? status}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
{/* Action bar */}
|
||||
{status === 'pending_review' && (
|
||||
<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" color="orange" variant="light" leftSection={<IconAlertTriangle size={15} />} onClick={() => openNote('correction')}>
|
||||
Request Correction
|
||||
</Button>
|
||||
<Button size="sm" color="red" variant="light" leftSection={<IconX size={15} />} onClick={() => openNote('reject')}>
|
||||
Reject
|
||||
</Button>
|
||||
<Button size="sm" color="teal" leftSection={<IconCheck size={15} />} onClick={() => performAction('approve', 'approved', 'approved')} loading={actionLoading === 'approve'}>
|
||||
Approve Application
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
{status !== 'pending_review' && (
|
||||
<Alert variant="light" color={STATUS_COLOR[status] ?? 'gray'}
|
||||
icon={status === 'approved' ? <IconCircleCheck size={17} /> : <IconAlertTriangle size={17} />}>
|
||||
This application has been <strong>{STATUS_LABEL[status] ?? status}</strong>.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Tabs defaultValue="profile" variant="outline" radius="md">
|
||||
<Tabs.List mb="md">
|
||||
<Tabs.Tab value="profile" leftSection={<IconUser size={16} />}>Applicant Profile</Tabs.Tab>
|
||||
<Tabs.Tab value="bst" leftSection={<IconShieldCheck size={16} />}>BST Certificates</Tabs.Tab>
|
||||
<Tabs.Tab value="medical" leftSection={<IconHeart size={16} />}>Medical</Tabs.Tab>
|
||||
<Tabs.Tab value="payment" leftSection={<IconCreditCard size={16} />}>Payment</Tabs.Tab>
|
||||
<Tabs.Tab value="issuance" leftSection={<IconCertificate size={16} />}>Issuance</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
{/* ── Applicant Profile ─────────────────────────────────────── */}
|
||||
<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.applicant.fullName}</Text>
|
||||
<Text fz="sm" c="dimmed">Seafarer No: {app.applicant.seafarerNumber}</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Divider mb="md" />
|
||||
<SectionLabel>Personal Information</SectionLabel>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md" mb="lg">
|
||||
<InfoRow label="Date of Birth" value={app.applicant.dob} />
|
||||
<InfoRow label="Nationality" value={app.applicant.nationality} />
|
||||
<InfoRow label="National ID" value={app.applicant.idNumber} />
|
||||
<InfoRow label="Phone" value={app.applicant.phone} />
|
||||
<InfoRow label="Email" value={app.applicant.email} />
|
||||
<InfoRow label="Address" value={app.applicant.address} />
|
||||
</SimpleGrid>
|
||||
<Divider mb="md" />
|
||||
<SectionLabel>Physical Characteristics</SectionLabel>
|
||||
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="md">
|
||||
<InfoRow label="Height" value={app.applicant.height} />
|
||||
<InfoRow label="Blood Type" value={app.applicant.bloodType} />
|
||||
<InfoRow label="Hair Color" value={app.applicant.hairColor} />
|
||||
<InfoRow label="Eye Color" value={app.applicant.eyeColor} />
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
</Tabs.Panel>
|
||||
|
||||
{/* ── BST Certificates ──────────────────────────────────────── */}
|
||||
<Tabs.Panel value="bst">
|
||||
<Stack gap="md">
|
||||
<Alert variant="light" color="blue" icon={<IconInfoCircle size={17} />}>
|
||||
Verify all 5 BST training certificates. PST and FPFF have 5-year renewal — check expiry dates are valid.
|
||||
</Alert>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
||||
{app.bst.map((cert) => (
|
||||
<Paper key={cert.key} withBorder radius="lg" p="md">
|
||||
<Group gap="sm" mb="sm" wrap="nowrap">
|
||||
<ThemeIcon size="lg" variant="light" color="blue" radius="md">
|
||||
<IconShieldCheck size={18} stroke={1.5} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700} fz="sm">{cert.short}</Text>
|
||||
<Text fz="xs" c="dimmed" lh={1.3}>{cert.label}</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Stack gap={4} mb="sm">
|
||||
<InfoRow label="Certificate No." value={cert.certNumber} />
|
||||
<InfoRow label="Issuer" value={cert.issuer} />
|
||||
<InfoRow label="Issue Date" value={cert.issueDate} />
|
||||
{cert.expiryDate && <InfoRow label="Expiry Date" value={cert.expiryDate} />}
|
||||
</Stack>
|
||||
<DocViewer fileName={cert.fileName} fileType={cert.fileType} label={`${cert.short} Certificate`} />
|
||||
</Paper>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
|
||||
{/* ── Medical ───────────────────────────────────────────────── */}
|
||||
<Tabs.Panel value="medical">
|
||||
<Paper withBorder radius="lg" p="xl">
|
||||
<Group gap="sm" mb="lg">
|
||||
<ThemeIcon size="xl" variant="light" color="pink" radius="lg"><IconHeart size={22} stroke={1.5} /></ThemeIcon>
|
||||
<Text fw={700} fz="lg">Medical Certificate</Text>
|
||||
</Group>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md" mb="lg">
|
||||
<InfoRow label="Certificate No." value={app.medical.certNumber} />
|
||||
<InfoRow label="Issuing Centre" value={app.medical.issuer} />
|
||||
<InfoRow label="Issue Date" value={app.medical.issueDate} />
|
||||
<InfoRow label="Expiry Date" value={app.medical.expiryDate} />
|
||||
</SimpleGrid>
|
||||
<Box style={{ maxWidth: rem(420) }}>
|
||||
<DocViewer fileName={app.medical.fileName} fileType={app.medical.fileType} label="Medical Certificate" />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Tabs.Panel>
|
||||
|
||||
{/* ── Payment ───────────────────────────────────────────────── */}
|
||||
<Tabs.Panel value="payment">
|
||||
<Stack gap="md">
|
||||
<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</Text>
|
||||
<Badge size="sm" variant="light" color={PAYMENT_COLOR[app.payment.status]} mt={2}>
|
||||
{app.payment.status.charAt(0).toUpperCase() + app.payment.status.slice(1)}
|
||||
</Badge>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
{/* Fee breakdown grouped */}
|
||||
<Paper withBorder radius="md" p="md" bg="gray.0" mb="lg">
|
||||
<Text fw={700} fz="sm" mb="md">Fee Breakdown</Text>
|
||||
{Object.entries(feeGroups).map(([group, fees], gi) => (
|
||||
<div key={group}>
|
||||
{gi > 0 && <Divider my="sm" />}
|
||||
<Text fz="xs" fw={700} tt="uppercase" c="blue.7" mb={6}>{group}</Text>
|
||||
{fees.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>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<Divider mt="sm" mb="xs" />
|
||||
<Group justify="space-between">
|
||||
<Text fz="sm" fw={800}>Total</Text>
|
||||
<Text fz="md" fw={800} c="blue">ETB {TOTAL.toFixed(2)}</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md" mb="lg">
|
||||
<InfoRow label="Payment Method" value={app.payment.method} />
|
||||
<InfoRow label="Transaction Reference" value={app.payment.ref} />
|
||||
<InfoRow label="Payment Date" value={app.payment.date} />
|
||||
</SimpleGrid>
|
||||
|
||||
<Box style={{ maxWidth: rem(420) }}>
|
||||
<DocViewer fileName={app.payment.receiptFileName} fileType={app.payment.receiptFileType} label="Payment Receipt" />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
|
||||
{/* ── Issuance ──────────────────────────────────────────────── */}
|
||||
<Tabs.Panel value="issuance">
|
||||
<Stack gap="md">
|
||||
<Alert variant="light" color={status === 'approved' ? 'teal' : 'yellow'} icon={<IconInfoCircle size={17} />}>
|
||||
{status === 'approved'
|
||||
? 'Application approved. The documents below will be generated and dispatched to the applicant.'
|
||||
: 'Approve the application to trigger document issuance.'}
|
||||
</Alert>
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md">
|
||||
{[
|
||||
{ icon: IconBook2, color: 'blue', label: 'Seaman Book', desc: 'Official EMA seafarer identity book', validity: '5 years' },
|
||||
{ icon: IconCertificate,color: 'teal', label: 'Basic Training Certificate (BTC)',desc: 'EMA-issued BTC for all 5 BST courses', validity: '5 years' },
|
||||
{ icon: IconShieldCheck,color: 'violet',label: 'BSID Card', desc: 'Biometric Seafarer ID Card', validity: '5 years' },
|
||||
].map(({ icon: Icon, color, label, desc, validity }) => (
|
||||
<Card key={label} withBorder radius="lg" p="md"
|
||||
style={{ borderColor: status === 'approved' ? `var(--mantine-color-${color}-3)` : 'var(--mantine-color-gray-2)' }}>
|
||||
<Group gap="sm" mb="sm" wrap="nowrap">
|
||||
<ThemeIcon size="xl" variant="light" color={status === 'approved' ? color : 'gray'} radius="lg">
|
||||
<Icon size={22} stroke={1.5} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700} fz="sm">{label}</Text>
|
||||
<Badge size="xs" variant="light" color={status === 'approved' ? 'teal' : 'yellow'} mt={2}>
|
||||
{status === 'approved' ? 'Ready to Issue' : 'Pending Approval'}
|
||||
</Badge>
|
||||
</div>
|
||||
</Group>
|
||||
<Text fz="xs" c="dimmed" mb="xs">{desc}</Text>
|
||||
<Group justify="space-between">
|
||||
<Text fz="xs" c="dimmed">Validity</Text>
|
||||
<Text fz="xs" fw={500}>{validity}</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
{/* Note modal */}
|
||||
<Modal
|
||||
opened={noteModalOpen}
|
||||
onClose={() => setNoteModalOpen(false)}
|
||||
title={<Group gap="xs"><IconBook2 size={18} /><Text fw={700}>{noteAction === 'correction' ? 'Request Correction' : 'Reject Application'}</Text></Group>}
|
||||
size="md" radius="lg"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text fz="sm" c="dimmed">
|
||||
{noteAction === 'correction' ? 'Describe what the applicant needs to correct or resubmit.' : 'State the reason for rejection. The applicant will be notified.'}
|
||||
</Text>
|
||||
<Textarea label="Note to Applicant" placeholder="Enter your note..." rows={4} value={note} onChange={(e) => setNote(e.currentTarget.value)} required />
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={() => setNoteModalOpen(false)}>Cancel</Button>
|
||||
<Button color={noteAction === 'correction' ? 'orange' : 'red'} disabled={!note.trim()} loading={actionLoading === noteAction}
|
||||
onClick={() => performAction(noteAction!, noteAction === 'correction' ? 'sent back for correction' : 'rejected', noteAction === 'correction' ? 'correction_required' : 'rejected')}>
|
||||
{noteAction === 'correction' ? 'Send Back' : 'Reject'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Stack>
|
||||
<Container size="lg" py="xl">
|
||||
<FeatureUnavailable
|
||||
title="Application review"
|
||||
description="This generic review screen is superseded by Licence Applications, which is connected to real data."
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default ApplicationReviewPage;
|
||||
|
||||
Reference in New Issue
Block a user