From e8d227af09e30c30f5e56a0c13b278947d83efc0 Mon Sep 17 00:00:00 2001 From: Mengisteab Date: Mon, 15 Jun 2026 13:54:50 +0000 Subject: [PATCH] added seafarer registeration --- .claude/settings.json | 4 +- .../seafarer/pages/SeafarerProfilePage.tsx | 701 ++++++++++++++++++ .../pages/SeafarerRegistrationPage.tsx | 546 ++++++++++++++ .../seafarer/pages/SeafarerRegistryPage.tsx | 379 ++++++++++ apps/portal/src/app/layouts/PortalLayout.tsx | 14 +- apps/portal/src/app/router.tsx | 6 + 6 files changed, 1647 insertions(+), 3 deletions(-) create mode 100644 apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx create mode 100644 apps/portal/src/app/features/seafarer/pages/SeafarerRegistrationPage.tsx create mode 100644 apps/portal/src/app/features/seafarer/pages/SeafarerRegistryPage.tsx diff --git a/.claude/settings.json b/.claude/settings.json index a4bef31ab..f985f0a8b 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -25,7 +25,9 @@ "Bash(grep -c '_$_1e42')", "Bash(node_modules/.bin/tsc -p apps/backoffice/tsconfig.json --noEmit)", "Bash(python3 -m json.tool)", - "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(json.dumps\\({k:d[k] for k in ['workspaces','name'] if k in d}, indent=2\\)\\)\")" + "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(json.dumps\\({k:d[k] for k in ['workspaces','name'] if k in d}, indent=2\\)\\)\")", + "Bash(node_modules/.bin/tsc --project apps/portal/tsconfig.app.json --noEmit)", + "Bash(node_modules/.bin/tsc --project apps/portal/tsconfig.json --noEmit)" ], "additionalDirectories": [ "/home/tria/projects/mengisteab/emaui" diff --git a/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx b/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx new file mode 100644 index 000000000..f1e7f00f6 --- /dev/null +++ b/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx @@ -0,0 +1,701 @@ +import { useEffect, useState } from 'react'; +import { + ActionIcon, + Alert, + Avatar, + Badge, + Box, + Button, + Card, + Divider, + Group, + Modal, + Paper, + Select, + SimpleGrid, + Skeleton, + Stack, + Table, + Tabs, + Text, + TextInput, + Textarea, + ThemeIcon, + Title, + rem, +} from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; +import { + IconAnchor, + IconArrowLeft, + IconBook, + IconBriefcase, + IconCertificate, + IconCheck, + IconClock, + IconEdit, + IconFileText, + IconHeartbeat, + IconHistory, + IconLayoutDashboard, + IconPlus, + IconPrinter, + IconShip, + IconUser, + IconX, +} from '@tabler/icons-react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { notify } from '@ema-platform/ui'; +import type { Seafarer } from './SeafarerRegistryPage'; + +// --------------------------------------------------------------------------- +// Extended profile types +// --------------------------------------------------------------------------- +interface TrainingRecord { + id: string; + course: string; + institution: string; + certNo: string; + issueDate: string; + expiry: string; + status: 'Approved' | 'Pending' | 'Expired'; +} + +interface MedicalRecord { + id: string; + examType: string; + issuedBy: string; + issueDate: string; + expiry: string; + result: 'Fit' | 'Unfit' | 'Conditional'; + remarks: string; +} + +interface SeaServiceRecord { + id: string; + vesselName: string; + vesselType: string; + rank: string; + flag: string; + from: string; + to: string; + engagementPort: string; +} + +interface CertificationRecord { + id: string; + name: string; + certNo: string; + issuedBy: string; + issueDate: string; + expiry: string; + type: string; + status: 'Valid' | 'Expired' | 'Pending'; +} + +interface HistoryEntry { + id: string; + action: string; + performedBy: string; + date: string; + notes: string; +} + +interface SeafarerProfile extends Seafarer { + dob: string; + nationalId: string; + passportNo: string; + bookNumber: string; + permanentAddress: string; + training: TrainingRecord[]; + medical: MedicalRecord[]; + seaService: SeaServiceRecord[]; + certifications: CertificationRecord[]; + history: HistoryEntry[]; +} + +// --------------------------------------------------------------------------- +// Dummy API — replace bodies with real fetch calls +// --------------------------------------------------------------------------- +async function fetchSeafarerProfile(id: string): Promise { + await new Promise((r) => setTimeout(r, 800)); + return { + id, + seafarerId: 'SF-2024-0001', + firstName: 'Abebe', + lastName: 'Girma', + email: 'abebe.g@email.com', + gender: 'Male', + nationality: 'Ethiopian', + mobile: '+251 911 234 567', + region: 'Addis Ababa', + registeredAt: '2024-01-10', + medicalStatus: 'Fit', + bookStatus: 'Active', + status: 'Active', + dob: '1988-03-15', + nationalId: 'ET-1234567', + passportNo: 'EP123456', + bookNumber: 'SB-2024-0001', + permanentAddress: 'Bole Sub-City, Woreda 03, House No. 456, Addis Ababa', + training: [ + { id: '1', course: 'Personal Survival Techniques', institution: 'Ethiopian Maritime Institute', certNo: 'PST-2023-0456', issueDate: '2023-01-10', expiry: '2028-01-14', status: 'Approved' }, + { id: '2', course: 'Fire Prevention and Fire Fighting', institution: 'Djibouti Maritime Academy', certNo: 'FFF-2023-0789', issueDate: '2023-03-05', expiry: '2028-03-07', status: 'Approved' }, + { id: '3', course: 'Elementary First Aid', institution: 'Ethiopian Maritime Institute', certNo: 'EFA-2023-0102', issueDate: '2023-01-10', expiry: '2028-01-10', status: 'Approved' }, + ], + medical: [ + { id: '1', examType: 'STCW Medical Certificate', issuedBy: 'EMA Medical Center', issueDate: '2023-06-15', expiry: '2025-06-15', result: 'Fit', remarks: 'No medical conditions noted.' }, + { id: '2', examType: 'Pre-Employment Medical', issuedBy: 'Addis Ababa General Hospital', issueDate: '2022-01-10', expiry: '2024-01-10', result: 'Fit', remarks: 'All tests within normal range.' }, + ], + seaService: [ + { id: '1', vesselName: 'MV Ethiopian Star', vesselType: 'Bulk Carrier', rank: 'Ordinary Seaman', flag: 'Ethiopia', from: '2022-03-01', to: '2023-02-28', engagementPort: 'Djibouti' }, + { id: '2', vesselName: 'MV Red Sea Express', vesselType: 'Container Ship', rank: 'Able Seaman', flag: 'Djibouti', from: '2023-04-01', to: '2024-03-31', engagementPort: 'Berbera' }, + ], + certifications: [ + { id: '1', name: 'STCW Basic Safety Training', certNo: 'BST-2023-0001', issuedBy: 'Ethiopian Maritime Authority', issueDate: '2023-01-15', expiry: '2028-01-15', type: 'STCW', status: 'Valid' }, + { id: '2', name: 'Certificate of Competency — Deck Rating', certNo: 'COC-2023-0234', issuedBy: 'Ethiopian Maritime Authority', issueDate: '2023-07-01', expiry: '2028-07-01', type: 'COC', status: 'Valid' }, + ], + history: [ + { id: '1', action: 'Profile Created', performedBy: 'System', date: '2024-01-10', notes: 'Initial registration submitted.' }, + { id: '2', action: 'Status → Active', performedBy: 'Admin Officer', date: '2024-01-15', notes: 'All documents verified and approved.' }, + { id: '3', action: 'Training Record Added', performedBy: 'Abebe Girma', date: '2024-02-20', notes: 'PST certificate uploaded.' }, + ], + }; +} + +async function updateSeafarerStatus(_id: string, _status: string): Promise { + await new Promise((r) => setTimeout(r, 600)); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- +const STATUS_COLOR: Record = { + Active: 'teal', Pending: 'yellow', Suspended: 'red', + Approved: 'teal', Expired: 'red', Valid: 'teal', + Fit: 'teal', Unfit: 'red', Conditional: 'orange', +}; + +function Chip({ value }: { value: string }) { + return {value}; +} + +function InfoField({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value || '—'} +
+ ); +} + +function SectionCard({ title, children, action }: { title: string; children: React.ReactNode; action?: React.ReactNode }) { + return ( + + + {title} + {action} + + + {children} + + ); +} + +// --------------------------------------------------------------------------- +// Tab: Overview +// --------------------------------------------------------------------------- +function OverviewTab({ profile, onStatusChange }: { profile: SeafarerProfile; onStatusChange: (s: 'Active' | 'Suspended') => void }) { + return ( + + + + + + + + + + + + + + + + + + + +
+ Reg. Status + +
+
+ Medical Status + +
+ +
+ Book Status + +
+
+ + + {profile.status !== 'Active' && ( + + )} + {profile.status !== 'Suspended' && ( + + )} + + +
+ + + Permanent Address + {profile.permanentAddress || '—'} + +
+ ); +} + +// --------------------------------------------------------------------------- +// Tab: Training +// --------------------------------------------------------------------------- +function TrainingTab({ records, onAdd }: { records: TrainingRecord[]; onAdd: () => void }) { + return ( + + + Training Records + + + + + + + {['Course', 'Institution', 'Cert. No.', 'Issue Date', 'Expiry', 'Status', 'Actions'].map((h) => ( + {h} + ))} + + + + {records.map((r) => ( + + {r.course} + {r.institution} + {r.certNo} + {r.issueDate} + {r.expiry} + + + + + + ))} + +
+ {records.length === 0 && No training records found.} +
+ ); +} + +// --------------------------------------------------------------------------- +// Tab: Medical +// --------------------------------------------------------------------------- +function MedicalTab({ records, onAdd }: { records: MedicalRecord[]; onAdd: () => void }) { + return ( + + + Medical Records + + + + + + + {['Exam Type', 'Issued By', 'Issue Date', 'Expiry', 'Result', 'Remarks', 'Actions'].map((h) => ( + {h} + ))} + + + + {records.map((r) => ( + + {r.examType} + {r.issuedBy} + {r.issueDate} + {r.expiry} + + {r.remarks} + + + + + ))} + +
+ {records.length === 0 && No medical records found.} +
+ ); +} + +// --------------------------------------------------------------------------- +// Tab: Sea Service +// --------------------------------------------------------------------------- +function SeaServiceTab({ records, onAdd }: { records: SeaServiceRecord[]; onAdd: () => void }) { + return ( + + + Sea Service Records + + + + + + + {['Vessel Name', 'Type', 'Rank', 'Flag', 'From', 'To', 'Engagement Port', 'Actions'].map((h) => ( + {h} + ))} + + + + {records.map((r) => ( + + {r.vesselName} + {r.vesselType} + {r.rank} + {r.flag} + {r.from} + {r.to} + {r.engagementPort} + + + + + ))} + +
+ {records.length === 0 && No sea service records found.} +
+ ); +} + +// --------------------------------------------------------------------------- +// Tab: Certifications +// --------------------------------------------------------------------------- +function CertificationsTab({ records, onAdd }: { records: CertificationRecord[]; onAdd: () => void }) { + return ( + + + Certifications + + + + + + + {['Certificate', 'Cert. No.', 'Type', 'Issued By', 'Issue Date', 'Expiry', 'Status', 'Actions'].map((h) => ( + {h} + ))} + + + + {records.map((r) => ( + + {r.name} + {r.certNo} + {r.type} + {r.issuedBy} + {r.issueDate} + {r.expiry} + + + + + + ))} + +
+ {records.length === 0 && No certifications found.} +
+ ); +} + +// --------------------------------------------------------------------------- +// Tab: History +// --------------------------------------------------------------------------- +function HistoryTab({ entries }: { entries: HistoryEntry[] }) { + return ( + + Activity History + + + {entries.map((e) => ( + + + + +
+ + {e.action} + by {e.performedBy} + + {e.date} + {e.notes && {e.notes}} +
+
+ ))} + {entries.length === 0 && No history found.} +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Add Record Modal (generic) +// --------------------------------------------------------------------------- +function AddTrainingModal({ opened, onClose }: { opened: boolean; onClose: () => void }) { + return ( + + + + + + + + + + + + +