From b86335588fd2810a565ae70795620b65f797d49d Mon Sep 17 00:00:00 2001 From: fitse-yotor Date: Thu, 2 Jul 2026 16:23:17 +0300 Subject: [PATCH] feat: simplify BST to single certificate and fetch dashboard from API - BST page: replace 5 individual certificate cards with one combined BST certificate upload (institution issues one cert covering all components) - Dashboard: fetch profile, seaman book, medical, and BST status from backend on load; show skeletons while loading, fall back to defaults on API failure; greet user by first name from auth store - Add /basic-safety-training route to portal router Co-Authored-By: Claude Sonnet 4.6 --- .../pages/BasicSafetyTrainingPage.tsx | 629 +++++++++--------- .../dashboard/pages/DashboardPage.tsx | 431 +++++++++--- apps/portal/src/app/router.tsx | 6 + 3 files changed, 658 insertions(+), 408 deletions(-) diff --git a/apps/portal/src/app/features/basic-safety-training/pages/BasicSafetyTrainingPage.tsx b/apps/portal/src/app/features/basic-safety-training/pages/BasicSafetyTrainingPage.tsx index 1b17e5913..caba3b94d 100644 --- a/apps/portal/src/app/features/basic-safety-training/pages/BasicSafetyTrainingPage.tsx +++ b/apps/portal/src/app/features/basic-safety-training/pages/BasicSafetyTrainingPage.tsx @@ -7,16 +7,14 @@ import { Card, FileButton, Group, + List, Modal, Paper, - Progress, - SimpleGrid, Stack, Text, TextInput, ThemeIcon, Title, - rem, } from '@mantine/core'; import { IconAlertTriangle, @@ -25,109 +23,26 @@ import { IconCheck, IconCircleCheck, IconDownload, - IconFileDescription, IconInfoCircle, IconRefresh, - IconShield, IconShieldCheck, IconTrash, IconUpload, - IconX, } from '@tabler/icons-react'; import { notify } from '@ema-platform/ui'; // --------------------------------------------------------------------------- -// Types & constants +// Types // --------------------------------------------------------------------------- -interface BSTItem { - key: string; - label: string; - shortLabel: string; - description: string; - modelCourse: string; - refreshYears: number; - required: boolean; -} - interface BSTRecord { - key: string; issuer: string; issueDate: string; expiryDate: string; - status: 'Valid' | 'Expiring' | 'Expired' | 'Pending Verification'; certNumber: string; fileName: string; + status: 'Valid' | 'Expiring' | 'Expired' | 'Pending Verification'; } -const BST_ITEMS: BSTItem[] = [ - { - key: 'pst', - label: 'Personal Survival Techniques', - shortLabel: 'PST', - description: 'Covers lifeboat/life-raft operation, survival at sea, and distress signals.', - modelCourse: 'IMO 1.19', - refreshYears: 5, - required: true, - }, - { - key: 'fpff', - label: 'Fire Prevention & Fire Fighting', - shortLabel: 'FPFF', - description: 'Covers fire prevention, detection, and fire-fighting on board vessels.', - modelCourse: 'IMO 1.20', - refreshYears: 5, - required: true, - }, - { - key: 'efa', - label: 'Elementary First Aid', - shortLabel: 'EFA', - description: 'Basic first-aid procedures, CPR, and medical emergency response.', - modelCourse: 'IMO 1.13', - refreshYears: 0, - required: true, - }, - { - key: 'pssr', - label: 'Personal Safety & Social Responsibility', - shortLabel: 'PSSR', - description: 'Shipboard safety culture, regulations, and working relationships.', - modelCourse: 'IMO 1.21', - refreshYears: 0, - required: true, - }, - { - key: 'shp', - label: 'Sexual Harassment Prevention Training', - shortLabel: 'SHPT', - description: 'Awareness and prevention of harassment in the maritime workplace.', - modelCourse: 'EMA National', - refreshYears: 0, - required: true, - }, -]; - -const MOCK_RECORDS: Record = { - pst: { - key: 'pst', - issuer: 'Bahirdar Maritime School', - issueDate: '2023-04-10', - expiryDate: '2028-04-09', - status: 'Valid', - certNumber: 'PST-2023-BMS-0421', - fileName: 'pst_certificate.pdf', - }, - fpff: { - key: 'fpff', - issuer: 'Bahirdar Maritime School', - issueDate: '2023-04-10', - expiryDate: '2028-04-09', - status: 'Valid', - certNumber: 'FPFF-2023-BMS-0421', - fileName: 'fpff_certificate.pdf', - }, -}; - const STATUS_COLOR: Record = { Valid: 'teal', Expiring: 'orange', @@ -135,27 +50,37 @@ const STATUS_COLOR: Record = { 'Pending Verification': 'yellow', }; -function daysUntil(dateStr: string) { - return Math.ceil((new Date(dateStr).getTime() - Date.now()) / (1000 * 60 * 60 * 24)); -} +const BST_COMPONENTS = [ + { label: 'Personal Survival Techniques', short: 'PST', course: 'IMO 1.19' }, + { label: 'Fire Prevention & Fire Fighting', short: 'FPFF', course: 'IMO 1.20' }, + { label: 'Elementary First Aid', short: 'EFA', course: 'IMO 1.13' }, + { label: 'Personal Safety & Social Responsibility', short: 'PSSR', course: 'IMO 1.21' }, + { label: 'Sexual Harassment Prevention', short: 'SHPT', course: 'EMA National' }, +]; function formatDate(dateStr: string) { - return new Date(dateStr).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); + return new Date(dateStr).toLocaleDateString('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + }); +} + +function daysUntil(dateStr: string) { + return Math.ceil((new Date(dateStr).getTime() - Date.now()) / (1000 * 60 * 60 * 24)); } // --------------------------------------------------------------------------- // Upload modal // --------------------------------------------------------------------------- function UploadModal({ - item, opened, onClose, onUploaded, }: { - item: BSTItem | null; opened: boolean; onClose: () => void; - onUploaded: (key: string, record: BSTRecord) => void; + onUploaded: (record: BSTRecord) => void; }) { const [file, setFile] = useState(null); const [issuer, setIssuer] = useState(''); @@ -166,75 +91,153 @@ function UploadModal({ const resetRef = useRef<() => void>(null); const reset = () => { - setFile(null); setIssuer(''); setCertNumber(''); setIssueDate(''); setExpiryDate(''); + setFile(null); + setIssuer(''); + setCertNumber(''); + setIssueDate(''); + setExpiryDate(''); resetRef.current?.(); }; const handleSubmit = async () => { - if (!file || !issuer || !certNumber || !issueDate) { - notify.error('Please fill all required fields and upload the certificate.'); + if (!file || !issuer || !certNumber || !issueDate || !expiryDate) { + notify.error('Please fill all required fields and upload the certificate file.'); return; } setSubmitting(true); await new Promise((r) => setTimeout(r, 1000)); setSubmitting(false); - onUploaded(item!.key, { - key: item!.key, + onUploaded({ issuer, issueDate, - expiryDate: expiryDate || '', - status: 'Pending Verification', + expiryDate, certNumber, fileName: file.name, + status: 'Pending Verification', }); - notify.success(`${item!.shortLabel} certificate submitted for verification.`); + notify.success('Basic Safety Training certificate submitted for verification.'); reset(); onClose(); }; return ( - { reset(); onClose(); }} title={`Upload ${item?.label}`} size="md" centered> - {item && ( - - } p="xs"> - {item.description} — Model Course: {item.modelCourse} - - setIssuer(e.currentTarget.value)} size="sm" /> - setCertNumber(e.currentTarget.value)} size="sm" /> - - setIssueDate(e.currentTarget.value)} size="sm" /> - {item.refreshYears > 0 && ( - setExpiryDate(e.currentTarget.value)} size="sm" /> - )} - -
- Certificate File * - {file ? ( - - - - {file.name} - - - - ) : ( - - {(props) => ( - - )} - - )} -
- - - - -
- )} + { + reset(); + onClose(); + }} + title="Upload Basic Safety Training Certificate" + size="md" + centered + > + + } p="xs"> + + Upload your combined BST certificate issued by an EMA-approved training institution. + The certificate must cover all 5 components (PST, FPFF, EFA, PSSR, SHPT). + + + + setIssuer(e.currentTarget.value)} + size="sm" + /> + setCertNumber(e.currentTarget.value)} + size="sm" + /> + + setIssueDate(e.currentTarget.value)} + size="sm" + /> + setExpiryDate(e.currentTarget.value)} + size="sm" + /> + + +
+ + Certificate File * + + {file ? ( + + + + + {file.name} + + + + + ) : ( + + {(props) => ( + + )} + + )} +
+ + + + + +
); } @@ -243,203 +246,191 @@ function UploadModal({ // Main page // --------------------------------------------------------------------------- export function BasicSafetyTrainingPage() { - const [records, setRecords] = useState>(MOCK_RECORDS); - const [modalItem, setModalItem] = useState(null); + const [record, setRecord] = useState(null); + const [modalOpen, setModalOpen] = useState(false); - const doneCount = BST_ITEMS.filter((i) => records[i.key]).length; - const allDone = doneCount === BST_ITEMS.length; - - const handleUploaded = (key: string, record: BSTRecord) => { - setRecords((prev) => ({ ...prev, [key]: record })); - }; + const days = record?.expiryDate ? daysUntil(record.expiryDate) : null; + const isExpiringSoon = days !== null && days <= 180 && days > 0; + const isExpired = days !== null && days <= 0; return ( {/* Header */}
- Basic Safety Training + Basic Safety Training Certificate - All seafarers must complete 5 mandatory BST certificates before joining a vessel (STCW Chapter VI). + STCW Chapter VI/1 — mandatory for all seafarers before joining a vessel.
- : } - > - {doneCount} / {BST_ITEMS.length} Complete - + {record && ( + } + > + {record.status} + + )}
- {/* Overall progress */} - - - Overall Completion - {Math.round((doneCount / BST_ITEMS.length) * 100)}% - - - {!allDone && ( - } p="sm"> - - You need all 5 certificates to apply for a Seaman Book. Missing: {BST_ITEMS.filter((i) => !records[i.key]).map((i) => i.shortLabel).join(', ')} - - - )} - {allDone && ( - } p="sm"> - - All 5 BST training certificates are complete. You can now apply for your Seaman Book & BTC — - the Basic Training Certificate (BTC) is issued by EMA alongside your Seaman Book after your application is approved. - - - )} - + {/* Expiry alert */} + {isExpired && ( + }> + + Your BST certificate has expired. Upload a renewed certificate to remain eligible. + + + )} + {isExpiringSoon && ( + }> + + Your BST certificate expires in {days} days. Renew before it lapses. + + + )} - {/* Certificate cards */} - - {BST_ITEMS.map((item) => { - const rec = records[item.key]; - const days = rec?.expiryDate ? daysUntil(rec.expiryDate) : null; - const needsRefresh = item.refreshYears > 0; + {/* Certificate card */} + {record ? ( + + + + + + +
+ Basic Safety Training (BST) + Combined certificate — all 5 STCW components +
+
+ + {record.status} + +
- return ( - - - - - {rec ? : } - -
- {item.shortLabel} - {item.modelCourse} -
-
- {rec ? ( - {rec.status} - ) : ( - Missing + + + Certificate Number + {record.certNumber} + + + Issuing Institution + {record.issuer} + + + Issue Date + {formatDate(record.issueDate)} + + + Expiry Date + + {formatDate(record.expiryDate)} + {days !== null && days > 0 && ( + ({days} days remaining) )} + + + + File + {record.fileName} + + + + + + + +
+ ) : ( + + + + + +
+ No BST Certificate Uploaded + + You must upload a valid Basic Safety Training certificate issued by an + EMA-approved institution before applying for a Seaman Book. + +
+ +
+
+ )} + + {/* Components covered */} + + + + Certificate Components (STCW VI/1) + + + A combined BST certificate from an EMA-approved institution covers all five components: + + + + + } + > + {BST_COMPONENTS.map((c) => ( + + + {c.short} + — {c.label} + {c.course} - - {item.label} - - {rec ? ( - - - Certificate No. - {rec.certNumber} - - - Issuer - {rec.issuer} - - - Issued - {formatDate(rec.issueDate)} - - {needsRefresh && rec.expiryDate && ( - <> - - Expires - - {formatDate(rec.expiryDate)} - - - {days !== null && ( - - )} - - )} - {needsRefresh && ( - - {item.refreshYears}-year refresh required - - )} - - - - - - ) : ( - - {item.description} - {needsRefresh && ( - - Requires {item.refreshYears}-year refresh - - )} - - - )} - - ); - })} -
+ + ))} + + {/* Info */} - - - About Basic Safety Training (STCW VI/1) + + + Validity & Renewal - - - - Basic Safety Training is mandatory for all seafarers regardless of department (Deck, Engine, or Catering). - PST and FPFF certificates require evidence of maintained competence every 5 years. - EFA and PSSR do not have a mandatory 5-year repeat under STCW. - - - - - Certificates must be from EMA-approved training institutions (e.g. Bahirdar Maritime School, Babugaya Maritime School). - EMA officers will verify authenticity before approving your Seaman Book application. - - - + + + BST certificates are typically valid for 5 years. PST and FPFF components + require evidence of maintained competence at the 5-year point (STCW Reg. VI/1). + EFA and PSSR do not have a mandatory 5-year revalidation under STCW but your + institution's combined certificate carries a unified expiry date. + Certificates must be from EMA-approved training institutions. + + - {/* Upload modal */} setModalItem(null)} - onUploaded={handleUploaded} + opened={modalOpen} + onClose={() => setModalOpen(false)} + onUploaded={(rec) => setRecord(rec)} />
); diff --git a/apps/portal/src/app/features/dashboard/pages/DashboardPage.tsx b/apps/portal/src/app/features/dashboard/pages/DashboardPage.tsx index c850542c1..a76a3cf8a 100644 --- a/apps/portal/src/app/features/dashboard/pages/DashboardPage.tsx +++ b/apps/portal/src/app/features/dashboard/pages/DashboardPage.tsx @@ -1,3 +1,4 @@ +import { useEffect, useRef, useState } from 'react'; import { Alert, Badge, @@ -7,16 +8,17 @@ import { Paper, Progress, SimpleGrid, + Skeleton, Stack, Text, ThemeIcon, Title, UnstyledButton, useMantineTheme, - rem, } from '@mantine/core'; import { IconAlertCircle, + IconBell, IconBook2, IconChevronRight, IconClipboardList, @@ -26,42 +28,154 @@ import { IconShip, IconShieldCheck, IconUserPlus, - IconBell, } from '@tabler/icons-react'; import type { Icon } from '@tabler/icons-react'; import { useNavigate } from 'react-router-dom'; +import { useApiMutation } from '@ema-platform/api'; +import { useAppSelector } from '../../../store/hooks'; // --------------------------------------------------------------------------- -// Mock profile completeness — replace with real store/API data +// Types // --------------------------------------------------------------------------- -const PROFILE_STEPS = [ - { label: 'Personal Information', done: true }, - { label: 'Document Upload', done: true }, - { label: 'Medical Certificate', done: false }, - { label: 'Basic Safety Training', done: false }, - { label: 'Seaman Book', done: false }, -]; +interface ProfileSummary { + id: string; + fullName: string; + completeness: number; // 0–100 + steps: { label: string; done: boolean }[]; +} -const BST_ITEMS = [ - { label: 'Personal Survival Techniques (PST)', done: true, expiry: '2028-04-10' }, - { label: 'Fire Prevention & Fire Fighting (FPFF)', done: true, expiry: '2028-04-10' }, - { label: 'Elementary First Aid (EFA)', done: false, expiry: null }, - { label: 'Personal Safety & Social Responsibility (PSSR)', done: false, expiry: null }, - { label: 'Sexual Harassment Prevention', done: false, expiry: null }, -]; +interface SeamanBookSummary { + status: 'Not Applied' | 'Pending' | 'Under Review' | 'Approved' | 'Rejected'; + applicationId?: string; + submittedAt?: string; +} -const ALERTS = [ - { id: 1, type: 'warning', message: 'Medical certificate expires in 45 days. Please renew before it lapses.', route: '/medical-certificate' }, - { id: 2, type: 'info', message: '3 Basic Safety Training certificates are missing. Complete them to apply for a Seaman Book.', route: '/basic-safety-training' }, -]; +interface MedicalSummary { + status: 'Valid' | 'Expiring' | 'Expired' | 'Not Uploaded'; + expiryDate?: string; + daysUntilExpiry?: number; +} +interface BSTSummary { + status: 'Complete' | 'Pending Verification' | 'Missing'; + certNumber?: string; + expiryDate?: string; +} + +interface DashboardData { + profile: ProfileSummary; + seamanBook: SeamanBookSummary; + medical: MedicalSummary; + bst: BSTSummary; +} + +// --------------------------------------------------------------------------- +// Fallback data shown when API is unavailable +// --------------------------------------------------------------------------- +const FALLBACK: DashboardData = { + profile: { + id: '', + fullName: '', + completeness: 40, + steps: [ + { label: 'Personal Information', done: true }, + { label: 'Document Upload', done: true }, + { label: 'Medical Certificate', done: false }, + { label: 'Basic Safety Training', done: false }, + { label: 'Seaman Book', done: false }, + ], + }, + seamanBook: { status: 'Not Applied' }, + medical: { status: 'Not Uploaded' }, + bst: { status: 'Missing' }, +}; + +const STATUS_COLOR_SEAMAN: Record = { + 'Not Applied': 'gray', + Pending: 'yellow', + 'Under Review': 'blue', + Approved: 'teal', + Rejected: 'red', +}; + +const STATUS_COLOR_MEDICAL: Record = { + Valid: 'teal', + Expiring: 'orange', + Expired: 'red', + 'Not Uploaded': 'gray', +}; + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- export function DashboardPage() { const navigate = useNavigate(); const theme = useMantineTheme(); + const user = useAppSelector((s) => s.auth.user); - const doneSteps = PROFILE_STEPS.filter((s) => s.done).length; - const completeness = Math.round((doneSteps / PROFILE_STEPS.length) * 100); - const bstDone = BST_ITEMS.filter((b) => b.done).length; + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + const [fetchProfile] = useApiMutation(); + const [fetchSeamanBook] = useApiMutation(); + const [fetchMedical] = useApiMutation(); + const [fetchBST] = useApiMutation(); + + const fetched = useRef(false); + + useEffect(() => { + if (fetched.current) return; + fetched.current = true; + + const load = async () => { + try { + const [profile, seamanBook, medical, bst] = await Promise.allSettled([ + fetchProfile({ url: '/profiles/me', method: 'GET' }).unwrap(), + fetchSeamanBook({ url: '/seaman-book/my', method: 'GET' }).unwrap(), + fetchMedical({ url: '/medical/my', method: 'GET' }).unwrap(), + fetchBST({ url: '/bst/my', method: 'GET' }).unwrap(), + ]); + + setData({ + profile: profile.status === 'fulfilled' ? profile.value : FALLBACK.profile, + seamanBook: seamanBook.status === 'fulfilled' ? seamanBook.value : FALLBACK.seamanBook, + medical: medical.status === 'fulfilled' ? medical.value : FALLBACK.medical, + bst: bst.status === 'fulfilled' ? bst.value : FALLBACK.bst, + }); + } catch { + setData(FALLBACK); + } finally { + setLoading(false); + } + }; + + load(); + }, [fetchProfile, fetchSeamanBook, fetchMedical, fetchBST]); + + const d = data ?? FALLBACK; + const alerts: { type: 'warning' | 'info'; message: string; route: string }[] = []; + + if (d.medical.status === 'Expiring' && d.medical.daysUntilExpiry != null) { + alerts.push({ + type: 'warning', + message: `Medical certificate expires in ${d.medical.daysUntilExpiry} days. Please renew before it lapses.`, + route: '/documents', + }); + } + if (d.medical.status === 'Expired') { + alerts.push({ + type: 'warning', + message: 'Your medical certificate has expired. Upload a renewed certificate immediately.', + route: '/documents', + }); + } + if (d.bst.status === 'Missing') { + alerts.push({ + type: 'info', + message: 'No Basic Safety Training certificate uploaded. BST is required before applying for a Seaman Book.', + route: '/basic-safety-training', + }); + } return ( @@ -69,13 +183,13 @@ export function DashboardPage() { - Welcome to the EMA Seafarer Portal + Welcome{user?.name?.en ? `, ${user.name.en.split(' ')[0]}` : ''} to the EMA Seafarer Portal Manage your seafarer profile, track certificates, apply for your Seaman Book @@ -95,9 +209,9 @@ export function DashboardPage() { {/* Alerts */} - {ALERTS.map((alert) => ( + {alerts.map((alert, i) => ( : } @@ -110,68 +224,153 @@ export function DashboardPage() { {/* Status cards */} - - - - + {loading ? ( + <> + + + + + + ) : ( + <> + + + + + + )} - {/* Profile completeness */} + {/* Registration checklist */} Registration Checklist - - {completeness}% complete - + {loading ? ( + + ) : ( + + {d.profile.completeness}% complete + + )} - - - {PROFILE_STEPS.map((step) => ( - - - - - - {step.label} - - {step.done && Done} - - ))} - + {loading ? ( + + {[1, 2, 3, 4, 5].map((n) => )} + + ) : ( + <> + + + {d.profile.steps.map((step) => ( + + + + + + {step.label} + + {step.done && ( + + Done + + )} + + ))} + + + )} - {/* BST tracker */} + {/* BST status */} Basic Safety Training - - {bstDone} / {BST_ITEMS.length} - + {!loading && ( + + {d.bst.status} + + )} - - {BST_ITEMS.map((item) => ( - - - - - - {item.label} + {loading ? ( + + {[1, 2, 3].map((n) => )} + + ) : d.bst.status !== 'Missing' ? ( + + {d.bst.certNumber && ( + + Certificate No. + {d.bst.certNumber} + + )} + {d.bst.expiryDate && ( + + Expires + + {new Date(d.bst.expiryDate).toLocaleDateString('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + })} + + + )} + + Covers PST, FPFF, EFA, PSSR, and SHPT — STCW Chapter VI/1 + + + ) : ( + + } p="sm"> + + No BST certificate on file. Upload your certificate to become eligible for a Seaman Book. - {item.done && item.expiry && ( - exp {item.expiry} - )} - {!item.done && ( - Missing - )} - - ))} - + + navigate('/basic-safety-training')}> + + Upload BST Certificate → + + + + )} @@ -179,25 +378,75 @@ export function DashboardPage() { Quick Actions - navigate('/seafarer-registration')} /> - navigate('/seaman-book')} /> - navigate('/certificates')} /> - navigate('/documents')} /> - navigate('/documents')} /> - navigate('/support')} /> + navigate('/seafarer-registration')} + /> + navigate('/seaman-book')} + /> + navigate('/certificates')} + /> + navigate('/documents')} + /> + navigate('/documents')} + /> + navigate('/support')} + /> ); } -function StatusCard({ label, value, icon: Icon, color }: { label: string; value: string; icon: Icon; color: string }) { +function StatusCard({ + label, + value, + icon: Icon, + color, +}: { + label: string; + value: string; + icon: Icon; + color: string; +}) { return (
- {value} - {label} + + {value} + + + {label} +
@@ -208,7 +457,7 @@ function StatusCard({ label, value, icon: Icon, color }: { label: string; value: } function QuickAction({ - icon: ActionIconCmp, + icon: ActionIcon, color, label, sub, @@ -225,11 +474,15 @@ function QuickAction({ - +
- {label} - {sub} + + {label} + + + {sub} +
diff --git a/apps/portal/src/app/router.tsx b/apps/portal/src/app/router.tsx index 361e1bf2a..cdfba2efc 100644 --- a/apps/portal/src/app/router.tsx +++ b/apps/portal/src/app/router.tsx @@ -25,6 +25,9 @@ import { SeamanBookPage } from './features/seaman-book/pages/SeamanBookPage'; import { SeamanBookApplicationPage } from './features/seaman-book/pages/SeamanBookApplicationPage'; import { NotificationsPage } from './features/notifications/pages/NotificationsPage'; +// Basic Safety Training +import { BasicSafetyTrainingPage } from './features/basic-safety-training/pages/BasicSafetyTrainingPage'; + // Phase 2 — CoC / CoP import { CertificatesPage } from './features/certificates/pages/CertificatesPage'; import { CoCApplicationPage } from './features/certificates/pages/CoCApplicationPage'; @@ -77,6 +80,9 @@ export const router = createBrowserRouter([ { path: '/seaman-book/apply', element: }, { path: '/notifications', element: }, + // Basic Safety Training + { path: '/basic-safety-training', element: }, + // Phase 2 — CoC / CoP { path: '/certificates', element: }, { path: '/certificates/apply', element: },