mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
- Added VesselRegistrationApplicationPage for submitting new vessel registrations. - Created VesselRegistrationPage to list user's registrations and navigate to details. - Implemented VesselRegistrationStatusPage to display registration status and download certificates. - Integrated mock data for registrations and certificates. - Updated navigation and routing to include vessel registration paths. - Added translations for vessel registration in English and Amharic. - Documented the vessel registration workflow and API integration notes.
223 lines
7.9 KiB
TypeScript
223 lines
7.9 KiB
TypeScript
import { useCallback, useState } from 'react';
|
|
import { AppShell, rem } from '@mantine/core';
|
|
import { useDisclosure } from '@mantine/hooks';
|
|
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { logout } from '@ema-platform/auth';
|
|
import { AppHeader, AppSidebar } from '@ema-platform/ui';
|
|
import type { NavItem } from '@ema-platform/ui';
|
|
import {
|
|
IconAnchor,
|
|
IconBook2,
|
|
IconChartBar,
|
|
IconCreditCard,
|
|
IconFileDescription,
|
|
IconHeart,
|
|
IconLayoutDashboard,
|
|
IconShieldCheck,
|
|
IconRubberStamp,
|
|
IconSettings,
|
|
IconShip,
|
|
IconUser,
|
|
IconUsers,
|
|
IconUserShield,
|
|
IconQuestionMark,
|
|
IconClipboardList,
|
|
IconReport,
|
|
IconReportAnalytics,
|
|
} from '@tabler/icons-react';
|
|
import { notify } from '@ema-platform/ui';
|
|
import { SUPPORTED_LANGUAGES } from '../i18n/config';
|
|
import { useAppDispatch, useAppSelector } from '../store/hooks';
|
|
|
|
const NAV_ITEMS: NavItem[] = [
|
|
{ to: '/dashboard', label: 'nav.dashboard', icon: IconLayoutDashboard },
|
|
{ to: '/um/user-management/dashboard', label: 'nav.userManagement', icon: IconUserShield },
|
|
{ to: '/seaman-book-queue', label: 'nav.seamanBookQueue', icon: IconBook2 },
|
|
{ to: '/coc-queue', label: 'nav.cocQueue', icon: IconShieldCheck },
|
|
{ to: '/endorsement-queue', label: 'nav.endorsementQueue', icon: IconRubberStamp },
|
|
{ to: '/vessel-registrations', label: 'nav.vesselRegistrations', icon: IconAnchor },
|
|
{ to: '/vessel-registration-report', label: 'nav.vesselRegistrationReport', icon: IconReportAnalytics },
|
|
{ to: '/vessel-transfers', label: 'nav.vesselTransfers', icon: IconShip },
|
|
{ to: '/seafarer-registry', label: 'nav.seafarerRegistry', icon: IconUsers },
|
|
// { to: '/applications', label: 'nav.applications', icon: IconFileDescription },
|
|
{ to: '/payment-config', label: 'nav.paymentConfig', icon: IconCreditCard },
|
|
{ to: '/analytics', label: 'nav.analytics', icon: IconChartBar },
|
|
{ to: '/medical-verification', label: 'nav.medicalVerification', icon: IconHeart },
|
|
{ to: '/questions', label: 'nav.questions', icon: IconQuestionMark },
|
|
{ to: '/exams', label: 'nav.exams', icon: IconClipboardList },
|
|
{ to: '/exam-results', label: 'nav.examResults', icon: IconReport },
|
|
{ to: '/configuration', label: 'nav.configuration', icon: IconSettings },
|
|
{ to: '/profile', label: 'nav.profile', icon: IconUser },
|
|
];
|
|
|
|
const HEADER_HEIGHT = 116;
|
|
|
|
export function BackofficeLayout() {
|
|
const { t } = useTranslation();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const dispatch = useAppDispatch();
|
|
const [opened, { toggle: toggleNav }] = useDisclosure();
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
const user = useAppSelector((state) => state.auth.user);
|
|
const layoutMode = useAppSelector((state) => state.preferences.layoutMode);
|
|
|
|
const displayName = user?.name?.en || user?.username || '';
|
|
const initials = displayName
|
|
? displayName.split(/\s+/).map((s) => s[0]).join('').toUpperCase().slice(0, 2)
|
|
: '?';
|
|
|
|
const handleLogout = useCallback(() => {
|
|
dispatch(logout());
|
|
navigate('/login');
|
|
}, [dispatch, navigate]);
|
|
|
|
const segments = location.pathname.split('/').filter(Boolean);
|
|
const crumbs = [
|
|
{ label: t('nav.dashboard'), path: '/dashboard' },
|
|
...segments
|
|
.map((_, i) => '/' + segments.slice(0, i + 1).join('/'))
|
|
.filter((path) => path !== '/dashboard')
|
|
.filter((path) => !path.startsWith('/um'))
|
|
.map((path) => ({ label: t('nav.dashboard'), path })),
|
|
];
|
|
|
|
const go = (item: NavItem) => {
|
|
if (item.soon) {
|
|
notify.info(`${t(item.label)} — coming soon.`);
|
|
return;
|
|
}
|
|
if (item.to) {
|
|
navigate(item.to);
|
|
}
|
|
};
|
|
|
|
const handleToggleCollapse = useCallback(() => {
|
|
setCollapsed((prev) => !prev);
|
|
}, []);
|
|
|
|
const isSidebar = layoutMode === 'sidebar';
|
|
|
|
return (
|
|
<AppShell
|
|
header={{ height: isSidebar ? 74 : HEADER_HEIGHT }}
|
|
navbar={isSidebar ? {
|
|
width: collapsed ? 72 : 264,
|
|
breakpoint: 'sm',
|
|
collapsed: { mobile: !opened },
|
|
} : undefined}
|
|
padding="lg"
|
|
>
|
|
<AppShell.Header
|
|
style={{
|
|
background: 'var(--mantine-color-body)',
|
|
borderBottom: '1px solid var(--mantine-color-gray-2)',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
}}
|
|
>
|
|
<div style={{ height: 74, flexShrink: 0, padding: '0 32px' }}>
|
|
<AppHeader
|
|
onToggleNav={toggleNav}
|
|
onToggleSidebar={isSidebar ? handleToggleCollapse : toggleNav}
|
|
navOpened={opened}
|
|
breadcrumbs={crumbs}
|
|
onNavigate={navigate}
|
|
onLogout={handleLogout}
|
|
userName={displayName || t('app.name')}
|
|
userInitials={initials}
|
|
supportedLanguages={SUPPORTED_LANGUAGES}
|
|
/>
|
|
</div>
|
|
|
|
{!isSidebar && (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: rem(2),
|
|
padding: '0 32px',
|
|
height: 42,
|
|
borderTop: '1px solid var(--mantine-color-gray-1)',
|
|
overflowX: 'auto',
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{NAV_ITEMS.map((item) => {
|
|
const active = !!item.to && (location.pathname === item.to || location.pathname.startsWith(`${item.to}/`));
|
|
const ItemIcon = item.icon;
|
|
return (
|
|
<button
|
|
key={item.to}
|
|
onClick={() => go(item)}
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: rem(6),
|
|
padding: '8px 16px',
|
|
border: 'none',
|
|
borderBottom: '2px solid',
|
|
borderBottomColor: active
|
|
? 'var(--mantine-color-blue-6)'
|
|
: 'transparent',
|
|
background: 'transparent',
|
|
color: active
|
|
? 'var(--mantine-color-blue-6)'
|
|
: 'var(--mantine-color-gray-6)',
|
|
fontWeight: active ? 600 : 500,
|
|
fontSize: rem(14),
|
|
whiteSpace: 'nowrap',
|
|
cursor: 'pointer',
|
|
transition: 'all 150ms ease',
|
|
height: '100%',
|
|
marginBottom: -1,
|
|
fontFamily: 'inherit',
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
if (!active) e.currentTarget.style.color = 'var(--mantine-color-blue-6)';
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
if (!active) e.currentTarget.style.color = 'var(--mantine-color-gray-6)';
|
|
}}
|
|
>
|
|
<ItemIcon size={18} stroke={1.6} />
|
|
<span>{t(item.label)}</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</AppShell.Header>
|
|
|
|
{isSidebar && (
|
|
<AppShell.Navbar
|
|
p={0}
|
|
style={{
|
|
overflow: 'hidden',
|
|
transition: 'width 200ms ease',
|
|
background: 'var(--mantine-color-body)',
|
|
borderRight: '1px solid var(--mantine-color-gray-2)',
|
|
}}
|
|
>
|
|
<AppSidebar
|
|
navItems={NAV_ITEMS}
|
|
collapsed={collapsed}
|
|
activePath={location.pathname}
|
|
onToggleCollapse={handleToggleCollapse}
|
|
onNavigate={go}
|
|
brandName={t('app.name')}
|
|
brandSubtitle={t('app.authority')}
|
|
/>
|
|
</AppShell.Navbar>
|
|
)}
|
|
|
|
<AppShell.Main>
|
|
<div key={location.pathname} className="ema-page-enter">
|
|
<Outlet />
|
|
</div>
|
|
</AppShell.Main>
|
|
</AppShell>
|
|
);
|
|
}
|