diff --git a/.github/scripts/scan-malware.js b/.github/scripts/scan-malware.js index 784f9f2ed..b7e283813 100644 --- a/.github/scripts/scan-malware.js +++ b/.github/scripts/scan-malware.js @@ -286,12 +286,12 @@ const RULES = [ id: "GLOBAL_NONCE_MARKER", severity: "CRITICAL", description: - "Sets a short global marker string (e.g. global['!']='8-3946') as an infection flag / re-execution guard", + "Sets a short global marker string (e.g. test(src) { return matchAll(src, [ // global['!'] = '8-3946' or global["x"] = "abc-123" /global\s*\[\s*['"][^'"]{0,5}['"]\s*\]\s*=\s*['"][0-9!@#$%^&*\-]{3,20}['"]/g, - // global['!']='...' with no spaces (minified form) + // /global\['[^']{0,5}'\]='[^']{2,20}'/g, ]); }, diff --git a/.gitignore b/.gitignore index 4cbce0b01..20b771e89 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ temp_interactive_push.bat apps/backoffice/public/_um/ apps/backoffice/public/tinymce/ +local-packages/iamui-extracted/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 850934ff5..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "user-management"] - path = user-management - url = git@github.com:Tria-plc/iamui.git diff --git a/apps/backoffice/src/app/features/analytics/pages/AnalyticsPage.tsx b/apps/backoffice/src/app/features/analytics/pages/AnalyticsPage.tsx new file mode 100644 index 000000000..ff3d66a02 --- /dev/null +++ b/apps/backoffice/src/app/features/analytics/pages/AnalyticsPage.tsx @@ -0,0 +1,255 @@ +import { + Badge, + Button, + Card, + Divider, + Group, + Paper, + Progress, + SimpleGrid, + Stack, + Table, + Text, + ThemeIcon, + Title, + rem, +} from '@mantine/core'; +import { + IconBook2, + IconCertificate, + IconChartBar, + IconDownload, + IconHeart, + IconId, + IconShieldCheck, + IconTrendingUp, + IconUsers, +} from '@tabler/icons-react'; + +// --------------------------------------------------------------------------- +// Mock statistics +// --------------------------------------------------------------------------- +const KPI = [ + { label: 'Total Seafarers', value: 1284, delta: '+12 this month', color: 'blue', icon: IconUsers }, + { label: 'Seaman Books Issued', value: 1102, delta: '85.8% of total', color: 'teal', icon: IconBook2 }, + { label: 'BTC Issued', value: 1098, delta: '85.5% of total', color: 'teal', icon: IconCertificate }, + { label: 'BSID Issued', value: 874, delta: '68.1% of total', color: 'violet', icon: IconId }, + { label: 'CoC / CoP Issued', value: 342, delta: '26.6% of total', color: 'orange', icon: IconShieldCheck }, + { label: 'Medical Expiring (90d)',value: 41, delta: 'Action required', color: 'red', icon: IconHeart }, +]; + +const MONTHLY_APPS = [ + { month: 'Jan', seamanBook: 18, coc: 4 }, + { month: 'Feb', seamanBook: 22, coc: 6 }, + { month: 'Mar', seamanBook: 28, coc: 9 }, + { month: 'Apr', seamanBook: 31, coc: 12 }, + { month: 'May', seamanBook: 26, coc: 8 }, + { month: 'Jun', seamanBook: 35, coc: 14 }, +]; + +const CERT_DISTRIBUTION = [ + { label: 'Seaman Book', count: 1102, pct: 86, color: 'blue' }, + { label: 'Basic Training Cert', count: 1098, pct: 85, color: 'teal' }, + { label: 'BSID', count: 874, pct: 68, color: 'violet' }, + { label: 'CoC / CoP', count: 342, pct: 27, color: 'orange' }, +]; + +const REVENUE = [ + { cert: 'Seaman Book', apps: 1102, feePerApp: 700, revenue: 771400 }, + { cert: 'BTC', apps: 1098, feePerApp: 400, revenue: 439200 }, + { cert: 'BSID', apps: 874, feePerApp: 250, revenue: 218500 }, + { cert: 'CoC / CoP', apps: 342, feePerApp: 1400, revenue: 478800 }, +]; +const TOTAL_REVENUE = REVENUE.reduce((s, r) => s + r.revenue, 0); + +const EXPIRY_WATCH = [ + { cert: 'Seaman Book', expiring30: 8, expiring90: 23, total: 1102 }, + { cert: 'BTC', expiring30: 6, expiring90: 19, total: 1098 }, + { cert: 'Medical Cert', expiring30: 12, expiring90: 41, total: 1284 }, + { cert: 'CoC / CoP', expiring30: 4, expiring90: 17, total: 342 }, +]; + +// --------------------------------------------------------------------------- +// Components +// --------------------------------------------------------------------------- +function KpiCard({ label, value, delta, color, icon: Icon }: typeof KPI[0]) { + return ( + + + + {value.toLocaleString()} + + {label} + {delta} + + ); +} + +// Simple bar chart using divs +function BarChart() { + const max = Math.max(...MONTHLY_APPS.map((m) => m.seamanBook + m.coc)); + return ( + + {MONTHLY_APPS.map((m) => ( + + {m.month} +
+
+
+
+ {m.seamanBook + m.coc} + + ))} + +
Seaman Book +
CoC / CoP + + + ); +} + +// --------------------------------------------------------------------------- +// Main page +// --------------------------------------------------------------------------- +export function AnalyticsPage() { + return ( + + +
+ Analytics & Reports + Overview of seafarer registrations, certificate issuance, and revenue — mock data for demonstration +
+ +
+ + {/* KPIs */} + + {KPI.map((k) => )} + + + + {/* Monthly applications bar chart */} + + + + + Monthly Applications (2025) + + + + + + {/* Certificate distribution */} + + + + Certificate Distribution + + + {CERT_DISTRIBUTION.map((c) => ( +
+ + + {c.label} + + + {c.count.toLocaleString()} + {c.pct}% + + + +
+ ))} +
+
+
+ + {/* Revenue breakdown */} + + + Revenue Breakdown by Certificate Type + Total: ETB {TOTAL_REVENUE.toLocaleString()} + + + + + {['Certificate Type', 'Applications', 'Fee per App', 'Total Revenue', '% of Revenue'].map((h) => ( + {h} + ))} + + + + {REVENUE.map((r) => ( + + {r.cert} + {r.apps.toLocaleString()} + ETB {r.feePerApp.toLocaleString()} + ETB {r.revenue.toLocaleString()} + + + + {((r.revenue / TOTAL_REVENUE) * 100).toFixed(1)}% + + + + ))} + + + + Total + {REVENUE.reduce((s, r) => s + r.apps, 0).toLocaleString()} + + ETB {TOTAL_REVENUE.toLocaleString()} + + + +
+
+ + {/* Expiry watchlist */} + + + + Expiry Watchlist + + + + + {['Certificate', 'Total Issued', 'Expiring in 30 days', 'Expiring in 90 days', '% at Risk'].map((h) => ( + {h} + ))} + + + + {EXPIRY_WATCH.map((e) => ( + + {e.cert} + {e.total.toLocaleString()} + + 5 ? 'red' : 'orange'} variant="light" size="sm">{e.expiring30} + + + 20 ? 'orange' : 'yellow'} variant="light" size="sm">{e.expiring90} + + + + 0.1 ? 'red' : 'orange'} + size="sm" + radius="xl" + style={{ flex: 1, minWidth: 60 }} + /> + {((e.expiring90 / e.total) * 100).toFixed(1)}% + + + + ))} + +
+
+
+ ); +} diff --git a/apps/backoffice/src/app/features/applications/pages/ApplicationReviewPage.tsx b/apps/backoffice/src/app/features/applications/pages/ApplicationReviewPage.tsx new file mode 100644 index 000000000..1ec7115d0 --- /dev/null +++ b/apps/backoffice/src/app/features/applications/pages/ApplicationReviewPage.tsx @@ -0,0 +1,446 @@ +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'; + +// --------------------------------------------------------------------------- +// 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 = { + pending_review: 'yellow', approved: 'teal', correction_required: 'orange', rejected: 'red', +}; +const STATUS_LABEL: Record = { + 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 ( + <> + + + + + +
+ {label} + {fileName} +
+ {fileType.toUpperCase()} +
+ + {fileType === 'pdf' + ?