mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 13:05:46 +00:00
ui componenet based on the requirements
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Group,
|
||||
Modal,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Switch,
|
||||
Table,
|
||||
Text,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
rem,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconBook2,
|
||||
IconCertificate,
|
||||
IconCheck,
|
||||
IconCreditCard,
|
||||
IconEdit,
|
||||
IconId,
|
||||
IconInfoCircle,
|
||||
IconShieldCheck,
|
||||
} from '@tabler/icons-react';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
interface CertFee {
|
||||
id: string;
|
||||
certType: string;
|
||||
icon: typeof IconBook2;
|
||||
color: string;
|
||||
description: string;
|
||||
fees: { label: string; amount: number; editable: boolean }[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface PaymentMethod {
|
||||
id: string;
|
||||
name: string;
|
||||
shortName: string;
|
||||
accountNumber: string;
|
||||
accountName: string;
|
||||
instructions: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Initial state
|
||||
// ---------------------------------------------------------------------------
|
||||
const INITIAL_CERT_FEES: CertFee[] = [
|
||||
{
|
||||
id: 'seaman-book',
|
||||
certType: 'Seaman Book',
|
||||
icon: IconBook2,
|
||||
color: 'blue',
|
||||
description: 'Official EMA seafarer identification book — valid 5 years',
|
||||
fees: [
|
||||
{ label: 'Application Fee', amount: 500, editable: true },
|
||||
{ label: 'Document Verification Fee',amount: 200, editable: true },
|
||||
],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'btc',
|
||||
certType: 'Basic Training Certificate (BTC)',
|
||||
icon: IconCertificate,
|
||||
color: 'teal',
|
||||
description: 'EMA-issued BTC certifying all 5 basic safety training courses — valid 5 years',
|
||||
fees: [
|
||||
{ label: 'Application Fee', amount: 300, editable: true },
|
||||
{ label: 'Document Verification Fee',amount: 100, editable: true },
|
||||
],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'bsid',
|
||||
certType: 'BSID (Biometric Seafarer ID)',
|
||||
icon: IconId,
|
||||
color: 'violet',
|
||||
description: 'Biometric Seafarer Identity Document — valid 5 years',
|
||||
fees: [
|
||||
{ label: 'Application Fee', amount: 100, editable: true },
|
||||
{ label: 'Card Production Fee', amount: 150, editable: true },
|
||||
],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'certificate',
|
||||
certType: 'Certificate (CoC / CoP)',
|
||||
icon: IconShieldCheck,
|
||||
color: 'orange',
|
||||
description: 'Certificate of Competency or Certificate of Proficiency under STCW',
|
||||
fees: [
|
||||
{ label: 'Application Fee', amount: 800, editable: true },
|
||||
{ label: 'Examination Fee', amount: 400, editable: true },
|
||||
{ label: 'Certificate Issuance Fee', amount: 200, editable: true },
|
||||
],
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const INITIAL_PAYMENT_METHODS: PaymentMethod[] = [
|
||||
{
|
||||
id: 'cbe',
|
||||
name: 'Commercial Bank of Ethiopia',
|
||||
shortName: 'CBE',
|
||||
accountNumber: '1000123456789',
|
||||
accountName: 'EMA Maritime Authority',
|
||||
instructions: 'Transfer the exact fee amount. Use your full name as the transfer description.',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'telebirr',
|
||||
name: 'Telebirr (Ethio Telecom)',
|
||||
shortName: 'Telebirr',
|
||||
accountNumber: '+251 11 551 0000',
|
||||
accountName: 'EMA Maritime Authority',
|
||||
instructions: 'Send to Telebirr number. Screenshot your confirmation and upload it with your application.',
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fee edit modal
|
||||
// ---------------------------------------------------------------------------
|
||||
function FeeEditModal({
|
||||
cert,
|
||||
opened,
|
||||
onClose,
|
||||
onSave,
|
||||
}: {
|
||||
cert: CertFee | null;
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (id: string, fees: CertFee['fees']) => void;
|
||||
}) {
|
||||
const [fees, setFees] = useState<CertFee['fees']>(cert?.fees ?? []);
|
||||
|
||||
const handleOpen = () => setFees(cert?.fees ?? []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={<Group gap="xs"><IconCreditCard size={18} /><Text fw={700}>Edit Fees — {cert?.certType}</Text></Group>}
|
||||
size="md"
|
||||
radius="lg"
|
||||
onTransitionEnd={() => { if (opened) handleOpen(); }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Alert variant="light" color="blue" icon={<IconInfoCircle size={15} />}>
|
||||
Changes take effect on new applications immediately. Existing applications retain the fee at time of submission.
|
||||
</Alert>
|
||||
{fees.map((fee, i) => (
|
||||
<Group key={fee.label} gap="sm" align="flex-end">
|
||||
<TextInput label="Fee Label" value={fee.label} disabled style={{ flex: 1 }} size="sm" />
|
||||
<NumberInput
|
||||
label="Amount (ETB)"
|
||||
value={fee.amount}
|
||||
min={0}
|
||||
step={50}
|
||||
style={{ width: rem(140) }}
|
||||
size="sm"
|
||||
disabled={!fee.editable}
|
||||
onChange={(v) =>
|
||||
setFees((prev) => prev.map((f, idx) => idx === i ? { ...f, amount: Number(v) || 0 } : f))
|
||||
}
|
||||
/>
|
||||
</Group>
|
||||
))}
|
||||
<Divider />
|
||||
<Paper withBorder radius="md" p="sm" bg="gray.0">
|
||||
<Group justify="space-between">
|
||||
<Text fz="sm" fw={700}>Total</Text>
|
||||
<Text fz="sm" fw={700} c="blue">ETB {fees.reduce((s, f) => s + f.amount, 0).toFixed(2)}</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={onClose}>Cancel</Button>
|
||||
<Button leftSection={<IconCheck size={15} />} onClick={() => { onSave(cert!.id, fees); onClose(); }}>
|
||||
Save Fees
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Payment method edit modal
|
||||
// ---------------------------------------------------------------------------
|
||||
function MethodEditModal({
|
||||
method,
|
||||
opened,
|
||||
onClose,
|
||||
onSave,
|
||||
}: {
|
||||
method: PaymentMethod | null;
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (updated: PaymentMethod) => void;
|
||||
}) {
|
||||
const [form, setForm] = useState<PaymentMethod>(method ?? INITIAL_PAYMENT_METHODS[0]);
|
||||
const set = (k: keyof PaymentMethod, v: string) => setForm((p) => ({ ...p, [k]: v }));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={<Group gap="xs"><IconCreditCard size={18} /><Text fw={700}>Edit Payment Method — {method?.shortName}</Text></Group>}
|
||||
size="md"
|
||||
radius="lg"
|
||||
onTransitionEnd={() => { if (opened && method) setForm(method); }}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<TextInput label="Account Number / Phone" value={form.accountNumber} onChange={(e) => set('accountNumber', e.currentTarget.value)} />
|
||||
<TextInput label="Account Name" value={form.accountName} onChange={(e) => set('accountName', e.currentTarget.value)} />
|
||||
<TextInput label="Instructions (shown to applicant)" value={form.instructions} onChange={(e) => set('instructions', e.currentTarget.value)} />
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={onClose}>Cancel</Button>
|
||||
<Button leftSection={<IconCheck size={15} />} onClick={() => { onSave(form); onClose(); }}>Save</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main page
|
||||
// ---------------------------------------------------------------------------
|
||||
export function PaymentConfigPage() {
|
||||
const [certFees, setCertFees] = useState<CertFee[]>(INITIAL_CERT_FEES);
|
||||
const [methods, setMethods] = useState<PaymentMethod[]>(INITIAL_PAYMENT_METHODS);
|
||||
|
||||
const [editingCert, setEditingCert] = useState<CertFee | null>(null);
|
||||
const [editingMethod, setEditingMethod] = useState<PaymentMethod | null>(null);
|
||||
|
||||
const totalCombined = certFees
|
||||
.filter((c) => c.enabled)
|
||||
.flatMap((c) => c.fees)
|
||||
.reduce((s, f) => s + f.amount, 0);
|
||||
|
||||
const handleSaveFees = (id: string, fees: CertFee['fees']) => {
|
||||
setCertFees((prev) => prev.map((c) => c.id === id ? { ...c, fees } : c));
|
||||
notify.success('Fees updated successfully.');
|
||||
};
|
||||
|
||||
const handleToggleCert = (id: string) => {
|
||||
setCertFees((prev) => prev.map((c) => c.id === id ? { ...c, enabled: !c.enabled } : c));
|
||||
};
|
||||
|
||||
const handleSaveMethod = (updated: PaymentMethod) => {
|
||||
setMethods((prev) => prev.map((m) => m.id === updated.id ? updated : m));
|
||||
notify.success('Payment method updated.');
|
||||
};
|
||||
|
||||
const handleToggleMethod = (id: string) => {
|
||||
setMethods((prev) => prev.map((m) => m.id === id ? { ...m, enabled: !m.enabled } : m));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Header */}
|
||||
<Group justify="space-between" align="flex-start">
|
||||
<div>
|
||||
<Title order={3}>Payment Configuration</Title>
|
||||
<Text fz="sm" c="dimmed">Manage certificate fees and accepted payment methods shown to applicants</Text>
|
||||
</div>
|
||||
<Badge size="lg" variant="light" color="blue">
|
||||
Combined Total: ETB {totalCombined.toFixed(2)}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
{/* Fee summary table */}
|
||||
<Paper withBorder radius="lg" p="xl">
|
||||
<Group justify="space-between" mb="lg">
|
||||
<Text fw={700} fz="md">Certificate Fees</Text>
|
||||
<Text fz="xs" c="dimmed">Click Edit to change fee amounts</Text>
|
||||
</Group>
|
||||
|
||||
<Table highlightOnHover verticalSpacing="sm" fz="sm">
|
||||
<Table.Thead bg="var(--mantine-color-default-hover)">
|
||||
<Table.Tr>
|
||||
{['Certificate Type', 'Fee Breakdown', 'Total', 'Status', ''].map((h) => (
|
||||
<Table.Th key={h} style={{ fontSize: rem(11), textTransform: 'uppercase', color: 'var(--mantine-color-dimmed)' }}>{h}</Table.Th>
|
||||
))}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{certFees.map((cert) => {
|
||||
const CertIcon = cert.icon;
|
||||
const total = cert.fees.reduce((s, f) => s + f.amount, 0);
|
||||
return (
|
||||
<Table.Tr key={cert.id} style={{ opacity: cert.enabled ? 1 : 0.5 }}>
|
||||
<Table.Td>
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<ThemeIcon size="md" variant="light" color={cert.color} radius="md">
|
||||
<CertIcon size={16} stroke={1.5} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fz="sm" fw={600}>{cert.certType}</Text>
|
||||
<Text fz="xs" c="dimmed">{cert.description}</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Stack gap={2}>
|
||||
{cert.fees.map((f) => (
|
||||
<Group key={f.label} gap={6}>
|
||||
<Text fz="xs" c="dimmed">{f.label}:</Text>
|
||||
<Text fz="xs" fw={500}>ETB {f.amount.toFixed(2)}</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz="sm" fw={700} c={cert.enabled ? 'blue.7' : 'dimmed'}>ETB {total.toFixed(2)}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Switch
|
||||
checked={cert.enabled}
|
||||
onChange={() => handleToggleCert(cert.id)}
|
||||
size="sm"
|
||||
color="teal"
|
||||
label={cert.enabled ? 'Active' : 'Disabled'}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon variant="light" color="blue" size="md" onClick={() => setEditingCert(cert)}>
|
||||
<IconEdit size={15} />
|
||||
</ActionIcon>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
<Table.Tfoot>
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={2}><Text fz="sm" fw={700} ta="right">Grand Total (all active)</Text></Table.Td>
|
||||
<Table.Td><Text fz="sm" fw={800} c="blue">ETB {totalCombined.toFixed(2)}</Text></Table.Td>
|
||||
<Table.Td colSpan={2} />
|
||||
</Table.Tr>
|
||||
</Table.Tfoot>
|
||||
</Table>
|
||||
</Paper>
|
||||
|
||||
{/* Payment methods */}
|
||||
<Paper withBorder radius="lg" p="xl">
|
||||
<Text fw={700} fz="md" mb="lg">Accepted Payment Methods</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
{methods.map((method) => (
|
||||
<Card key={method.id} withBorder radius="md" p="md" style={{ opacity: method.enabled ? 1 : 0.6 }}>
|
||||
<Group justify="space-between" mb="sm">
|
||||
<Group gap="sm">
|
||||
<ThemeIcon size="lg" variant="light" color={method.enabled ? 'blue' : 'gray'} radius="md">
|
||||
<IconCreditCard size={18} stroke={1.5} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700} fz="sm">{method.name}</Text>
|
||||
<Badge size="xs" variant="light" color={method.enabled ? 'teal' : 'gray'}>
|
||||
{method.enabled ? 'Active' : 'Disabled'}
|
||||
</Badge>
|
||||
</div>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Switch checked={method.enabled} onChange={() => handleToggleMethod(method.id)} size="sm" color="teal" />
|
||||
<ActionIcon variant="light" color="blue" size="md" onClick={() => setEditingMethod(method)}>
|
||||
<IconEdit size={15} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
<Divider mb="sm" />
|
||||
<Stack gap={4}>
|
||||
<Group justify="space-between">
|
||||
<Text fz="xs" c="dimmed">Account / Number</Text>
|
||||
<Text fz="xs" fw={600}>{method.accountNumber}</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text fz="xs" c="dimmed">Account Name</Text>
|
||||
<Text fz="xs" fw={600}>{method.accountName}</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
<Text fz="xs" c="dimmed" mt="sm" lh={1.4}>{method.instructions}</Text>
|
||||
</Card>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
|
||||
{/* Fee edit modal */}
|
||||
<FeeEditModal
|
||||
cert={editingCert}
|
||||
opened={!!editingCert}
|
||||
onClose={() => setEditingCert(null)}
|
||||
onSave={handleSaveFees}
|
||||
/>
|
||||
|
||||
{/* Method edit modal */}
|
||||
<MethodEditModal
|
||||
method={editingMethod}
|
||||
opened={!!editingMethod}
|
||||
onClose={() => setEditingMethod(null)}
|
||||
onSave={handleSaveMethod}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user