Adding license generation functionality

This commit is contained in:
Muluhabt
2026-07-29 16:53:50 +03:00
parent 26cc1f4fd2
commit 5161e278ce
102 changed files with 5995 additions and 21326 deletions

View File

@@ -5,7 +5,7 @@ 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 type { NavItem, NavSection } from '@ema-platform/ui';
import {
IconBook2,
IconChartBar,
@@ -27,6 +27,7 @@ import {
IconGauge,
IconShieldOff,
IconShip,
IconMapPin,
IconStack2,
IconTruck,
} from '@tabler/icons-react';
@@ -34,36 +35,66 @@ 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: '/vessel-registration-head-dashboard', label: 'nav.vesselRegistrationHeadDashboard', icon: IconGauge },
{ to: '/vessel-registration-queue', label: 'nav.vesselRegistrationQueue', icon: IconAnchor },
{ to: '/vessel-registration-queue/new', label: 'nav.vesselFormBuilder', icon: IconFilePlus },
{ to: '/vessel-registration-report', label: 'nav.vesselRegistrationReport', icon: IconChartBar },
{ to: '/vessel-ownership-transfer', label: 'nav.ownershipTransferQueue', icon: IconFileDescription },
{ to: '/logistics-head-dashboard', label: 'nav.logisticsHeadDashboard', icon: IconGauge },
{ to: '/freight-forwarder-license', label: 'nav.freightForwarderLicense', icon: IconTruck },
{ to: '/shipping-agent-license', label: 'nav.shippingAgentLicense', icon: IconShip },
{ to: '/combined-license', label: 'nav.combinedLicense', icon: IconStack2 },
{ to: '/joint-investment-license', label: 'nav.jointInvestmentLicense', icon: IconUsers },
{ to: '/mto-license', label: 'nav.mtoLicense', icon: IconTruck },
{ to: '/waiver', label: 'nav.waiver', icon: IconShieldOff },
{ to: '/coc-queue', label: 'nav.cocQueue', icon: IconShieldCheck },
{ to: '/endorsement-queue', label: 'nav.endorsementQueue', icon: IconRubberStamp },
{ 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 },
/**
* Grouped so a reviewer can find things, and flagged so they can tell what
* actually works: `soon` marks screens with no backend behind them yet.
*/
const NAV_SECTIONS: NavSection[] = [
{
items: [{ to: '/dashboard', label: 'nav.dashboard', icon: IconLayoutDashboard }],
},
{
label: 'nav.groupLicensing',
items: [
{ to: '/licence-review', label: 'nav.licenceReview', icon: IconTruck },
{ to: '/logistics-head-dashboard', label: 'nav.logisticsHeadDashboard', icon: IconGauge },
{ to: '/payment-config', label: 'nav.paymentConfig', icon: IconCreditCard },
{ to: '/waiver', label: 'nav.waiver', icon: IconShieldOff, soon: true },
],
},
{
label: 'nav.groupSeafarer',
items: [
{ to: '/seafarer-registry', label: 'nav.seafarerRegistry', icon: IconUsers },
{ to: '/coc-queue', label: 'nav.cocQueue', icon: IconShieldCheck, soon: true },
{ to: '/seaman-book-queue', label: 'nav.seamanBookQueue', icon: IconBook2, soon: true },
{ to: '/endorsement-queue', label: 'nav.endorsementQueue', icon: IconRubberStamp, soon: true },
{ to: '/medical-verification', label: 'nav.medicalVerification', icon: IconHeart, soon: true },
],
},
{
label: 'nav.groupExaminations',
items: [
{ to: '/questions', label: 'nav.questions', icon: IconQuestionMark },
{ to: '/exams', label: 'nav.exams', icon: IconClipboardList },
{ to: '/exam-results', label: 'nav.examResults', icon: IconReport },
],
},
{
label: 'nav.groupVessels',
items: [
{ to: '/vessel-registration-queue', label: 'nav.vesselRegistrationQueue', icon: IconAnchor, soon: true },
{ to: '/vessel-ownership-transfer', label: 'nav.ownershipTransferQueue', icon: IconFileDescription, soon: true },
{ to: '/vessel-registration-queue/new', label: 'nav.vesselFormBuilder', icon: IconFilePlus, soon: true },
{ to: '/vessel-registration-report', label: 'nav.vesselRegistrationReport', icon: IconChartBar, soon: true },
{ to: '/vessel-registration-head-dashboard', label: 'nav.vesselRegistrationHeadDashboard', icon: IconGauge, soon: true },
],
},
{
label: 'nav.groupAdministration',
items: [
{ to: '/um/user-management/dashboard', label: 'nav.userManagement', icon: IconUserShield },
{ to: '/configuration', label: 'nav.configuration', icon: IconSettings },
{ to: '/locations', label: 'nav.locations', icon: IconMapPin },
{ to: '/analytics', label: 'nav.analytics', icon: IconChartBar, soon: true },
{ to: '/profile', label: 'nav.profile', icon: IconUser },
],
},
];
/** Flat list used for breadcrumbs and active-route lookup. */
const NAV_ITEMS: NavItem[] = NAV_SECTIONS.flatMap((section) => section.items);
const HEADER_HEIGHT = 116;
export function BackofficeLayout() {
@@ -87,13 +118,28 @@ export function BackofficeLayout() {
}, [dispatch, navigate]);
const segments = location.pathname.split('/').filter(Boolean);
// Label each crumb from the nav item it corresponds to, falling back to a
// readable form of the path segment. Every crumb was previously labelled
// "Dashboard", which made the trail useless.
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 })),
.map((path) => {
const match = NAV_ITEMS.find((item) => item.to === path);
if (match) return { label: t(match.label), path };
const segment = path.split('/').pop() ?? '';
// Ids get a generic label rather than a raw uuid in the trail.
const isId = /^[0-9a-f-]{8,}$/i.test(segment) || /^\d+$/.test(segment);
return {
label: isId
? t('nav.details', 'Details')
: segment.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
path,
};
}),
];
const go = (item: NavItem) => {
@@ -164,7 +210,9 @@ export function BackofficeLayout() {
<button
key={item.to}
onClick={() => go(item)}
title={item.soon ? `${t(item.label)}${t('nav.soon', 'Soon')}` : undefined}
style={{
opacity: item.soon ? 0.55 : 1,
display: 'flex',
alignItems: 'center',
gap: rem(6),
@@ -214,7 +262,7 @@ export function BackofficeLayout() {
}}
>
<AppSidebar
navItems={NAV_ITEMS}
navItems={NAV_SECTIONS}
collapsed={collapsed}
activePath={location.pathname}
onToggleCollapse={handleToggleCollapse}