backoffice

This commit is contained in:
Fistum
2026-08-21 13:09:40 +00:00
parent 6b5b99f42d
commit 62492e957b
29 changed files with 392 additions and 487 deletions

View File

@@ -44,6 +44,7 @@ export function CertificateRequirementsPage() {
'certReq.subtitle',
'Configure the form fields and document uploads applicants must complete for a licence type, including conditions on when a requirement applies.',
)}
noMargin
/>
{isError ? (

View File

@@ -1,20 +1,9 @@
import { useState } from 'react';
import {
Stack,
Title,
Group,
Button,
Modal,
Text,
TextInput,
Textarea,
Card,
Alert,
} from '@mantine/core';
import {Stack, Button, Modal, Text, TextInput, Textarea, Card} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useTranslation } from 'react-i18next';
import { IconPlus, IconInfoCircle } from '@tabler/icons-react';
import { notify, useErrorHandler, AdvancedTable, useServerTable, ModalFooter } from '@ema-platform/ui';
import {IconPlus} from '@tabler/icons-react';
import { AdvancedTable, ErrorState, ModalFooter, notify, PageHeader, useErrorHandler, useServerTable } from '@ema-platform/ui';
import {
useGetCertificationsQuery,
useCreateCertificationMutation,
@@ -120,7 +109,8 @@ export function CertificationPage() {
}
};
if (isError) return <Alert icon={<IconInfoCircle size={16} />} color="red" title={t('certification.loadError')} />;
if (isError)
return <ErrorState title={t('certification.loadError')} onRetry={refetch} />;
const columns = [
...certificationColumns(t, locale),
@@ -134,17 +124,18 @@ export function CertificationPage() {
return (
<Stack gap="lg">
<Group justify="space-between" align="flex-end">
<div>
<Title order={2}>{t('certification.title')}</Title>
<Text fz="sm" c="dimmed">{t('certification.subtitle')}</Text>
</div>
{!showForm && (
<Button variant="light" leftSection={<IconPlus size={16} />} onClick={() => setShowForm(true)} size="sm">
{t('certification.add')}
</Button>
)}
</Group>
<PageHeader
title={t('certification.title')}
subtitle={t('certification.subtitle')}
noMargin
action={
!showForm && (
<Button variant="light" leftSection={<IconPlus size={16} />} onClick={() => setShowForm(true)} size="sm">
{t('certification.add')}
</Button>
)
}
/>
{showForm && (
<CertificationForm

View File

@@ -46,6 +46,7 @@ import {
import type { Profession } from "../../types/configuration";
import { professionColumns } from "./columns";
import { professionActionsColumn } from "./actions";
import { PageHeader } from '@ema-platform/ui';
interface ProfFormValues {
nameEn: string;
@@ -378,7 +379,7 @@ export function ConfigurationPage() {
return (
<Stack gap="lg">
<Title order={2}>{t("configuration.title")}</Title>
<PageHeader title={t("configuration.title")} noMargin />
<Tabs defaultValue="professions">
<Tabs.List>

View File

@@ -1,17 +1,8 @@
import { useNavigate } from 'react-router-dom';
import {
Card,
Center,
Container,
Group,
Loader,
SimpleGrid,
Text,
Title,
} from '@mantine/core';
import {Card, Center, Container, Group, Loader, SimpleGrid, Text} from '@mantine/core';
import { IconChevronRight } from '@tabler/icons-react';
import { useGetAssignedToMeQuery, useGetQueueQuery } from '@ema-platform/api';
import { AdvancedTable, PageLoader, useServerTable } from '@ema-platform/ui';
import { AdvancedTable, PageHeader, PageLoader, useServerTable } from '@ema-platform/ui';
import { dashboardQueueColumns } from './columns';
/**
@@ -53,12 +44,10 @@ export function DashboardPage() {
return (
<Container size="xl" py="md">
<Title order={3} mb="xs">
Dashboard
</Title>
<Text size="sm" c="dimmed" mb="lg">
Licence applications currently in the system.
</Text>
<PageHeader
title="Dashboard"
subtitle="Licence applications currently in the system."
/>
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} mb="xl">
{stats.map((stat) => (

View File

@@ -291,7 +291,7 @@ export function ExamDetailPage() {
<IconArrowLeft size={18} />
</ActionIcon>
<div>
<Title order={3}>{exam.title[locale]}</Title>
<Title order={2}>{exam.title[locale]}</Title>
</div>
</Group>
<Group gap="sm">

View File

@@ -2,7 +2,6 @@ import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
Stack,
Title,
Group,
Button,
Modal,
@@ -10,7 +9,6 @@ import {
TextInput,
Textarea,
Card,
Alert,
Select,
NumberInput,
Tabs,
@@ -35,6 +33,7 @@ import {
import type { Exam } from "../../types/exam";
import { examColumns } from "./columns";
import { examActionsColumn } from "./actions";
import { ErrorState, PageHeader } from '@ema-platform/ui';
function ExamForm({
editing,
@@ -416,13 +415,7 @@ export function ExamPage() {
};
if (isError)
return (
<Alert
icon={<IconInfoCircle size={16} />}
color="red"
title={t("exam.loadError")}
/>
);
return <ErrorState title={t("exam.loadError")} onRetry={refetch} />;
const columns = [
...examColumns(t, locale, getCertName, (exam) => navigate(`/exams/${exam.id}`)),
@@ -443,26 +436,25 @@ export function ExamPage() {
return (
<Stack gap="lg">
<Group justify="space-between" align="flex-end">
<div>
<Title order={2}>{t("exam.title")}</Title>
<Text fz="sm" c="dimmed">
{t("exam.subtitle")}
</Text>
</div>
{!showForm && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
<Button
variant="light"
leftSection={<IconPlus size={16} />}
onClick={() => setShowForm(true)}
size="sm"
>
{t("exam.add")}
</Button>
</RequirePermission>
)}
</Group>
<PageHeader
title={t("exam.title")}
subtitle={t("exam.subtitle")}
noMargin
action={
!showForm && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.MANAGE_EXAMS]} hideOnly>
<Button
variant="light"
leftSection={<IconPlus size={16} />}
onClick={() => setShowForm(true)}
size="sm"
>
{t("exam.add")}
</Button>
</RequirePermission>
)
}
/>
{showForm && (
<ExamForm

View File

@@ -1,10 +1,11 @@
import { Stack, Title, Paper } from '@mantine/core';
import {Stack, Paper} from '@mantine/core';
import { ItemTable } from '../components/ItemTable';
import { PageHeader } from '@ema-platform/ui';
export function ItemPage() {
return (
<Stack gap="lg">
<Title order={2}>Items</Title>
<PageHeader title="Items" noMargin />
<Paper p="md" shadow="sm" radius="md" withBorder>
<ItemTable />
</Paper>

View File

@@ -1,19 +1,7 @@
import { useState } from 'react';
import {
Button,
Card,
Container,
Group,
Modal,
Select,
Stack,
Text,
TextInput,
Textarea,
Title,
} from '@mantine/core';
import {Button, Card, Container, Group, Modal, Select, Stack, Text, TextInput, Textarea} from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { AdvancedTable, notify, useServerTable } from '@ema-platform/ui';
import { AdvancedTable, notify, PageHeader, useServerTable } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
import {
extractErrorMessage,
@@ -151,21 +139,19 @@ export function LicenseRegisterPage() {
return (
<Container size="xl" py="md">
<Group justify="space-between" mb="md">
<div>
<Title order={3}>Licence register</Title>
<Text size="sm" c="dimmed">
{data?.total ?? 0} issued licence{(data?.total ?? 0) === 1 ? '' : 's'}
</Text>
</div>
<TextInput
placeholder="Certificate № or company"
leftSection={<IconSearch size={14} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
w={280}
/>
</Group>
<PageHeader
title="Licence register"
subtitle={`${data?.total ?? 0} issued licence${(data?.total ?? 0) === 1 ? '' : 's'}`}
action={
<TextInput
placeholder="Certificate № or company"
leftSection={<IconSearch size={14} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
w={280}
/>
}
/>
<Card withBorder padding={0}>
<AdvancedTable<IssuedLicense>

View File

@@ -17,7 +17,6 @@ import {
Tabs,
Text,
TextInput,
Title,
} from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import {
@@ -72,6 +71,7 @@ import { useAppDispatch, useAppSelector } from "../../../../store/hooks";
import { KEYBOARD_SHORTCUTS, useQueueKeyboard } from "../../useQueueKeyboard";
import { licenseQueueColumns } from "./columns";
import { licenseQueueActionsColumn } from "./actions";
import { PageHeader } from '@ema-platform/ui';
const PAGE_SIZE = 10;
const SEARCH_DEBOUNCE_MS = 300;
@@ -501,16 +501,11 @@ export function LicenseQueuePage() {
return (
<Container size="xl" py="md" pb={selected.length ? 80 : "md"}>
<Group justify="space-between" mb="md">
<div>
<Title order={3}>{queueTitle}</Title>
{typeCode && (
<Text size="sm" c="dimmed">
{t(`nav.type${typeCode}`, { defaultValue: typeCode })}
</Text>
)}
</div>
<Group gap="xs">
<PageHeader
title={queueTitle}
subtitle={typeCode ? t(`nav.type${typeCode}`, { defaultValue: typeCode }) : undefined}
action={
<Group gap="xs">
<SegmentedControl
size="xs"
value={density}
@@ -534,8 +529,9 @@ export function LicenseQueuePage() {
>
{t("queue.export", "Export CSV")}
</Button>
</Group>
</Group>
</Group>
}
/>
{/* Saved views, counted. */}
<Tabs

View File

@@ -724,7 +724,7 @@ export function LicenseReviewPage() {
<Container size="xl" py="md">
<Group justify="space-between" mb="md">
<div>
<Title order={3}>{headerName}</Title>
<Title order={2}>{headerName}</Title>
<Group gap="xs">
<Text size="sm" c="dimmed">
{app.applicationNumber}

View File

@@ -1,23 +1,9 @@
import { useState, useCallback } from 'react';
import {
Stack,
Title,
Group,
Button,
Paper,
Text,
Grid,
Modal,
ActionIcon,
Tooltip,
Loader,
Center,
Alert,
} from '@mantine/core';
import {Stack, Group, Button, Paper, Text, Grid, Modal, ActionIcon, Tooltip, Loader, Center, Alert} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconSettings, IconMap, IconInfoCircle, IconPlus } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import { notify, useErrorHandler, ModalFooter, PageLoader } from '@ema-platform/ui';
import { ModalFooter, notify, PageHeader, PageLoader, useErrorHandler } from '@ema-platform/ui';
import { LocationTree } from '../components/LocationTree';
import { LocationDetail } from '../components/LocationDetail';
import { LocationForm } from '../components/LocationForm';
@@ -108,9 +94,11 @@ export function LocationPage() {
return (
<Stack gap="lg">
<Group justify="space-between">
<Title order={2}>{t('location.title')}</Title>
<Group gap="sm">
<PageHeader
title={t('location.title')}
noMargin
action={
<Group gap="sm">
{locationTypes.length > 0 && (
<Button
variant="light"
@@ -135,8 +123,9 @@ export function LocationPage() {
<IconSettings size={20} />
</ActionIcon>
</Tooltip>
</Group>
</Group>
</Group>
}
/>
{locationTypes.length === 0 && (
<Alert

View File

@@ -1,19 +1,7 @@
import { useNavigate } from 'react-router-dom';
import {
Badge,
Card,
Center,
Container,
Grid,
Group,
Loader,
SimpleGrid,
Stack,
Text,
Title,
} from '@mantine/core';
import {Badge, Card, Center, Container, Grid, Group, Loader, SimpleGrid, Stack, Text} from '@mantine/core';
import { IconChevronRight } from '@tabler/icons-react';
import { AdvancedTable, useServerTable, PageLoader } from '@ema-platform/ui';
import { AdvancedTable, PageHeader, PageLoader, useServerTable } from '@ema-platform/ui';
import {
STATUS_COLORS,
STATUS_LABELS,
@@ -74,12 +62,10 @@ export function LogisticsHeadDashboardPage() {
return (
<Container size="xl" py="md">
<Title order={3} mb="xs">
Logistics overview
</Title>
<Text size="sm" c="dimmed" mb="lg">
Licence applications currently in the department.
</Text>
<PageHeader
title="Logistics overview"
subtitle="Licence applications currently in the department."
/>
<SimpleGrid cols={{ base: 1, sm: 2, md: 4 }} mb="xl">
{stats.map((stat) => (

View File

@@ -1,27 +1,8 @@
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Badge,
Button,
Center,
Container,
Group,
Loader,
Modal,
Paper,
SegmentedControl,
Stack,
Text,
Textarea,
Title,
} from '@mantine/core';
import {Badge, Button, Center, Container, Group, Loader, Modal, Paper, SegmentedControl, Stack, Text, Textarea} from '@mantine/core';
import { IconEye, IconInbox, IconPaperclip } from '@tabler/icons-react';
import {
AdvancedTable,
notify,
PdfPreviewModal,
type AdvancedColumn,
} from '@ema-platform/ui';
import { AdvancedTable, notify, PageHeader, PdfPreviewModal, type AdvancedColumn } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
import {
extractErrorMessage,
@@ -376,22 +357,24 @@ export function MedicalVerificationPage({ kind = 'medical' }: { kind?: Verificat
return (
<Container size="xl" py="md">
<Title order={3} mb={4}>
{isMedical
? t('recordVerification.medicalTitle', 'Medical Certificate Verification')
: t('recordVerification.seaServiceTitle', 'Sea Service Verification')}
</Title>
<Text size="sm" c="dimmed" mb="md">
{isMedical
? t(
'recordVerification.medicalSubtitle',
'Submitted medical certificates awaiting a ruling. Verified certificates are frozen; rejections return to the seafarer with your remark.',
)
: t(
'recordVerification.seaServiceSubtitle',
'Submitted sea-service records awaiting a ruling. Verified records are frozen and count toward sea time; rejections return to the seafarer with your remark.',
)}
</Text>
<PageHeader
title={
isMedical
? t('recordVerification.medicalTitle', 'Medical Certificate Verification')
: t('recordVerification.seaServiceTitle', 'Sea Service Verification')
}
subtitle={
isMedical
? t(
'recordVerification.medicalSubtitle',
'Submitted medical certificates awaiting a ruling. Verified certificates are frozen; rejections return to the seafarer with your remark.',
)
: t(
'recordVerification.seaServiceSubtitle',
'Submitted sea-service records awaiting a ruling. Verified records are frozen and count toward sea time; rejections return to the seafarer with your remark.',
)
}
/>
{isMedical ? (
<AdvancedTable

View File

@@ -1,37 +1,13 @@
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Alert,
Badge,
Button,
Center,
Group,
Loader,
Modal,
NumberInput,
Paper,
Stack,
Switch,
Text,
TextInput,
ThemeIcon,
Title,
Tooltip,
} from '@mantine/core';
import {Alert, Badge, Button, Center, Group, Loader, Modal, NumberInput, Paper, Stack, Switch, Text, TextInput, ThemeIcon, Tooltip} from '@mantine/core';
import {
IconAlertTriangle,
IconCreditCard,
IconInfoCircle,
IconLock,
} from '@tabler/icons-react';
import {
notify,
ModalFooter,
AdvancedTable,
useServerTable,
type AdvancedColumn,
PageLoader,
} from '@ema-platform/ui';
import { AdvancedTable, ModalFooter, notify, PageHeader, PageLoader, useServerTable, type AdvancedColumn } from '@ema-platform/ui';
import {
extractErrorMessage,
useLocalized,
@@ -92,20 +68,19 @@ export function PaymentConfigPage() {
return (
<Stack gap="lg">
<Group justify="space-between" align="flex-start">
<div>
<Title order={3}>{t('paymentConfig.title', 'Payment configuration')}</Title>
<Text size="sm" c="dimmed" mt={4}>
{t(
'paymentConfig.subtitle',
'What each licence costs. Applicants are charged after approval, and the amount is fixed onto the application at that moment.',
)}
</Text>
</div>
<ThemeIcon size="xl" radius="md" variant="light">
<IconCreditCard size={22} />
</ThemeIcon>
</Group>
<PageHeader
title={t('paymentConfig.title', 'Payment configuration')}
subtitle={t(
'paymentConfig.subtitle',
'What each licence costs. Applicants are charged after approval, and the amount is fixed onto the application at that moment.',
)}
noMargin
action={
<ThemeIcon size="xl" radius="md" variant="light">
<IconCreditCard size={22} />
</ThemeIcon>
}
/>
<Alert
variant="light"

View File

@@ -247,7 +247,7 @@ export function ProfilePage() {
return (
<Stack gap="lg" maw={900}>
<PageHeader title={t('profile.title')} subtitle={t('profile.subtitle')} />
<PageHeader title={t('profile.title')} subtitle={t('profile.subtitle')} noMargin />
{/* Profile summary */}
<Paper p="lg" shadow="sm" radius="lg" withBorder>

View File

@@ -1,22 +1,9 @@
import { useState } from 'react';
import {
Stack,
Title,
Group,
Button,
Badge,
Modal,
Text,
TextInput,
Alert,
Select,
NumberInput,
Textarea,
} from '@mantine/core';
import {Stack, Group, Button, Badge, Modal, Text, TextInput, Select, NumberInput, Textarea} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useTranslation } from 'react-i18next';
import { IconPlus, IconInfoCircle } from '@tabler/icons-react';
import { AdvancedColumn, AdvancedTable, ModalFooter, notify, useErrorHandler, useServerTable } from '@ema-platform/ui';
import {IconPlus} from '@tabler/icons-react';
import { AdvancedColumn, AdvancedTable, ErrorState, ModalFooter, notify, PageHeader, useErrorHandler, useServerTable } from '@ema-platform/ui';
import { extractErrorMessage } from '@ema-platform/api';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import { useGetCertificationsQuery } from '../../../certification/api/certification-api';
@@ -201,7 +188,7 @@ export function QuestionPage() {
}
};
if (isError) return <Alert icon={<IconInfoCircle size={16} />} color="red" title={t('question.loadError')} />;
if (isError) return <ErrorState title={t('question.loadError')} onRetry={refetch} />;
const columns: AdvancedColumn<Question>[] = [
...questionColumns(t, { locale, getCertName }),
@@ -216,16 +203,19 @@ export function QuestionPage() {
return (
<Stack gap="lg">
<Group justify="space-between" align="flex-end">
<Title order={2}>{t('question.title')}</Title>
{!showForm && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.AUTHOR_QUESTION]} hideOnly>
<Button variant="light" leftSection={<IconPlus size={16} />} onClick={() => setShowForm(true)} size="sm">
{t('question.addQuestion')}
</Button>
</RequirePermission>
)}
</Group>
<PageHeader
title={t('question.title')}
noMargin
action={
!showForm && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.AUTHOR_QUESTION]} hideOnly>
<Button variant="light" leftSection={<IconPlus size={16} />} onClick={() => setShowForm(true)} size="sm">
{t('question.addQuestion')}
</Button>
</RequirePermission>
)
}
/>
{showForm && (
<QuestionForm

View File

@@ -1,21 +1,8 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Alert,
Button,
Center,
Group,
Loader,
Modal,
Paper,
Select,
Stack,
Text,
Textarea,
Title,
} from '@mantine/core';
import {Alert, Button, Center, Group, Loader, Modal, Paper, Select, Stack, Text, Textarea} from '@mantine/core';
import { IconInfoCircle } from '@tabler/icons-react';
import { AdvancedTable, notify, useServerTable, PageLoader } from '@ema-platform/ui';
import { AdvancedTable, notify, PageHeader, PageLoader, useServerTable } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
import { extractErrorMessage } from '@ema-platform/api';
import {
@@ -75,12 +62,11 @@ export function ExamAppealsPage() {
return (
<Stack gap="lg">
<div>
<Title order={2}>{t('result.appeals.title')}</Title>
<Text fz="sm" c="dimmed">
{t('result.appeals.subtitle')}
</Text>
</div>
<PageHeader
title={t('result.appeals.title')}
subtitle={t('result.appeals.subtitle')}
noMargin
/>
<Paper withBorder radius="md">
<AdvancedTable<ExamAppeal>

View File

@@ -1,39 +1,9 @@
import { useState, useCallback, type ElementType } from 'react';
import { useTranslation } from 'react-i18next';
import {
Stack,
Title,
Group,
Table,
Badge,
Modal,
Text,
Paper,
Loader,
Center,
Alert,
Select,
SimpleGrid,
Divider,
Button,
ThemeIcon,
TextInput,
} from '@mantine/core';
import {Stack, Group, Table, Badge, Modal, Text, Paper, Loader, Center, Select, SimpleGrid, Divider, Button, ThemeIcon, TextInput} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import {
IconInfoCircle,
IconUser,
IconCertificate,
IconDeviceFloppy,
IconPlus,
IconClipboardList,
IconCircleCheck,
IconCircleX,
IconChartBar,
IconSearch,
IconSend,
} from '@tabler/icons-react';
import { notify, BilingualInput, useErrorHandler, AdvancedTable, StatusBadge, useServerTable, ModalFooter } from '@ema-platform/ui';
import {IconUser, IconCertificate, IconDeviceFloppy, IconPlus, IconClipboardList, IconCircleCheck, IconCircleX, IconChartBar, IconSearch, IconSend} from '@tabler/icons-react';
import { AdvancedTable, BilingualInput, ErrorState, ModalFooter, notify, PageHeader, StatusBadge, useErrorHandler, useServerTable } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
import type { BilingualValue } from '@ema-platform/ui';
import { extractErrorMessage, useLocalized } from '@ema-platform/api';
@@ -279,7 +249,7 @@ export function ResultPage() {
}
};
if (isError) return <Alert icon={<IconInfoCircle size={16} />} color="red" title={t('result.loadError')} />;
if (isError) return <ErrorState title={t('result.loadError')} onRetry={refetch} />;
const columns = [
...resultColumns(t, locale, showDate, getExamTitle),
@@ -294,12 +264,12 @@ export function ResultPage() {
return (
<Stack gap="lg">
<Group justify="space-between" align="flex-end">
<div>
<Title order={2}>{t('result.title')}</Title>
<Text fz="sm" c="dimmed">{t('result.subtitle')}</Text>
</div>
<Group gap="sm">
<PageHeader
title={t('result.title')}
subtitle={t('result.subtitle')}
noMargin
action={
<Group gap="sm">
<RequirePermission anyOf={[LICENSE_PERMISSIONS.PUBLISH_EXAM_RESULT]} hideOnly>
<Button
variant="light"
@@ -316,8 +286,9 @@ export function ResultPage() {
<Button leftSection={<IconPlus size={15} />} onClick={openPicker} size="sm">
{t('result.record')}
</Button>
</Group>
</Group>
</Group>
}
/>
<SimpleGrid cols={{ base: 2, lg: 4 }} spacing="lg">
<ResultStat label={t('result.stats.totalResults')} value={String(total)} icon={IconClipboardList} color="blue" />

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {Badge, Container, Select, Text, TextInput, Title} from '@mantine/core';
import {Badge, Container, Select, Text, TextInput} from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { useDebouncedValue } from '@mantine/hooks';
import {
@@ -12,7 +12,7 @@ import {
type SeafarerDocumentRow,
type SeafarerDocumentStatus,
} from '@ema-platform/api';
import { AdvancedTable, type AdvancedColumn } from '@ema-platform/ui';
import { AdvancedTable, PageHeader, WaitingFor, type AdvancedColumn } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
const PAGE_SIZE = 10;
@@ -91,6 +91,17 @@ export function SeafarerDocumentQueuePage({ kind }: { kind: SeafarerDocumentKind
<Text size="sm">{row.original.submittedAt ? showDate(row.original.submittedAt) : '—'}</Text>
),
},
{
header: 'Waiting',
accessorKey: 'submittedAt',
size: 90,
cell: ({ row }) => (
<WaitingFor
since={row.original.submittedAt}
done={row.original.status === 'ISSUED' || row.original.status === 'REJECTED'}
/>
),
},
{
header: 'Status',
accessorKey: 'status',
@@ -106,13 +117,10 @@ export function SeafarerDocumentQueuePage({ kind }: { kind: SeafarerDocumentKind
return (
<Container size="xl" py="md">
<Title order={3} mb={4}>
{SEAFARER_DOCUMENT_KIND_LABELS[kind]} Queue
</Title>
<Text size="sm" c="dimmed" mb="md">
Requests released by an approved seafarer registration: confirm payment, schedule the
collection date, then issue.
</Text>
<PageHeader
title={`${SEAFARER_DOCUMENT_KIND_LABELS[kind]} Queue`}
subtitle="Requests released by an approved seafarer registration: confirm payment, schedule the collection date, then issue."
/>
<AdvancedTable
columns={columns}
data={data?.items ?? []}

View File

@@ -1,21 +1,6 @@
import { useState } from 'react';
import { Link, useNavigate, useParams } from 'react-router-dom';
import {
Alert,
Badge,
Button,
Center,
Container,
Group,
Loader,
Modal,
Paper,
Stack,
Table,
Text,
Textarea,
Title,
} from '@mantine/core';
import {Alert, Badge, Button, Center, Container, Group, Loader, Modal, Paper, Stack, Table, Text, Textarea} from '@mantine/core';
import { IconAlertTriangle, IconArrowLeft, IconCheck, IconDownload } from '@tabler/icons-react';
import {
SEAFARER_DOCUMENT_KIND_LABELS,
@@ -29,7 +14,7 @@ import {
useRejectSeafarerDocumentMutation,
useScheduleSeafarerDocumentMutation,
} from '@ema-platform/api';
import { AmharicDatePicker, notify } from '@ema-platform/ui';
import { AmharicDatePicker, notify, PageHeader } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
@@ -123,12 +108,10 @@ export function SeafarerDocumentReviewPage() {
>
Back to queue
</Button>
<Group justify="space-between" align="flex-start" mb="md">
<div>
<Title order={3}>
{kindLabel} {applicant?.name ?? '—'}
</Title>
<Group gap="xs" mt={4}>
<PageHeader
title={`${kindLabel}${applicant?.name ?? '—'}`}
meta={
<>
<Text size="sm" c="dimmed" ff="monospace">
{document.requestNumber}
</Text>
@@ -140,9 +123,10 @@ export function SeafarerDocumentReviewPage() {
{document.documentNumber}
</Badge>
)}
</Group>
</div>
<Group gap="xs">
</>
}
action={
<Group gap="xs">
{(document.status === 'PAYMENT_PENDING' || document.status === 'PAID') && (
<RequirePermission anyOf={[LICENSE_PERMISSIONS.APPROVE_APPLICATION]} hideOnly>
<Button loading={confirming} onClick={() => run(() => confirmPayment(id).unwrap(), 'Payment confirmed')}>
@@ -174,8 +158,9 @@ export function SeafarerDocumentReviewPage() {
</Button>
</RequirePermission>
)}
</Group>
</Group>
</Group>
}
/>
{document.status === 'REJECTED' && (
<Alert color="red" icon={<IconAlertTriangle size={16} />} title="Rejected" mb="md">
@@ -189,7 +174,7 @@ export function SeafarerDocumentReviewPage() {
<Text fw={600} size="sm" mb={4}>
Seafarer
</Text>
<Table withTableBorder withColumnBorders>
<Table variant="vertical">
<Table.Tbody>
<Row label="Name" value={applicant?.name} />
<Row label="Seafarer №" value={applicant?.seafarerNumber} />
@@ -212,7 +197,7 @@ export function SeafarerDocumentReviewPage() {
<Text fw={600} size="sm" mb={4}>
Payment
</Text>
<Table withTableBorder withColumnBorders>
<Table variant="vertical">
<Table.Tbody>
<Row label="Fee" value={document.feeAmount !== null ? `${document.feeAmount} ${document.feeCurrency}` : null} />
<Row label="Released to payment" value={document.submittedAt ? showDate(document.submittedAt) : null} />
@@ -226,7 +211,7 @@ export function SeafarerDocumentReviewPage() {
<Text fw={600} size="sm" mb={4}>
Issuance
</Text>
<Table withTableBorder withColumnBorders>
<Table variant="vertical">
<Table.Tbody>
<Row label="Pickup date" value={document.scheduledIssuanceDate ? showDate(document.scheduledIssuanceDate) : null} />
<Row label="Document №" value={document.documentNumber} />

View File

@@ -1,6 +1,6 @@
import { useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {Container, Select, Text, TextInput, Title} from '@mantine/core';
import {Container, Select, Text, TextInput} from '@mantine/core';
import { IconSearch } from '@tabler/icons-react';
import { useDebouncedValue } from '@mantine/hooks';
import {
@@ -11,7 +11,7 @@ import {
type SeafarerRegistration,
type SeafarerRegistrationStatus,
} from '@ema-platform/api';
import { AdvancedTable, StatusBadge, type AdvancedColumn } from '@ema-platform/ui';
import { AdvancedTable, PageHeader, StatusBadge, WaitingFor, type AdvancedColumn } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
const PAGE_SIZE = 10;
@@ -78,6 +78,17 @@ export function SeafarerRegistrationQueuePage() {
<Text size="sm">{row.original.submittedAt ? showDate(row.original.submittedAt) : '—'}</Text>
),
},
{
header: 'Waiting',
accessorKey: 'submittedAt',
size: 90,
cell: ({ row }) => (
<WaitingFor
since={row.original.submittedAt}
done={row.original.status === 'APPROVED' || row.original.status === 'REJECTED'}
/>
),
},
{
header: 'Status',
accessorKey: 'status',
@@ -94,13 +105,10 @@ export function SeafarerRegistrationQueuePage() {
return (
<Container size="xl" py="md">
<Title order={3} mb={4}>
Seafarer Registration Queue
</Title>
<Text size="sm" c="dimmed" mb="md">
Registrations awaiting review. Approval numbers the seafarer and opens their Seaman Book and
BTC applications.
</Text>
<PageHeader
title="Seafarer Registration Queue"
subtitle="Registrations awaiting review. Approval numbers the seafarer and opens their Seaman Book and BTC applications."
/>
<AdvancedTable
columns={columns}
data={data?.items ?? []}

View File

@@ -1,22 +1,6 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import {
Alert,
Badge,
Button,
Center,
Container,
Divider,
Group,
Loader,
Modal,
Paper,
Stack,
Table,
Text,
Textarea,
Title,
} from '@mantine/core';
import {Alert, Badge, Button, Center, Container, Divider, Group, Loader, Modal, Paper, Stack, Table, Text, Textarea} from '@mantine/core';
import { IconAlertTriangle, IconArrowLeft, IconCheck } from '@tabler/icons-react';
import {
SEAFARER_REGISTRATION_DOCUMENTS,
@@ -31,7 +15,7 @@ import {
useRejectSeafarerRegistrationMutation,
useRequestSeafarerRegistrationChangesMutation,
} from '@ema-platform/api';
import { notify, StatusBadge } from '@ema-platform/ui';
import { notify, PageHeader, StatusBadge } from '@ema-platform/ui';
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
import { applicantName } from './SeafarerRegistrationQueuePage';
@@ -109,10 +93,10 @@ export function SeafarerRegistrationReviewPage() {
<Button variant="subtle" size="xs" leftSection={<IconArrowLeft size={14} />} onClick={() => navigate('/seafarer-registrations')} mb="xs">
Back to queue
</Button>
<Group justify="space-between" align="flex-start" mb="md">
<div>
<Title order={3}>{applicantName(registration)}</Title>
<Group gap="xs" mt={4}>
<PageHeader
title={applicantName(registration)}
meta={
<>
<Text size="sm" c="dimmed" ff="monospace">
{registration.registrationNumber}
</Text>
@@ -125,9 +109,10 @@ export function SeafarerRegistrationReviewPage() {
{registration.seafarerNumber}
</Badge>
)}
</Group>
</div>
<Group gap="xs">
</>
}
action={
<Group gap="xs">
{canDecide && (
<>
<RequirePermission anyOf={[LICENSE_PERMISSIONS.REQUEST_ADJUSTMENT]} hideOnly>
@@ -147,8 +132,9 @@ export function SeafarerRegistrationReviewPage() {
</RequirePermission>
</>
)}
</Group>
</Group>
</Group>
}
/>
{registration.status === 'RESUBMIT_REQUIRED' && (
<Alert color="orange" icon={<IconAlertTriangle size={16} />} title="Awaiting the applicant's corrections" mb="md">
@@ -168,7 +154,7 @@ export function SeafarerRegistrationReviewPage() {
<Text fw={600} size="sm" mb={4}>
{section.title}
</Text>
<Table withTableBorder withColumnBorders>
<Table variant="vertical">
<Table.Tbody>
{section.fields
.filter((f) => f !== 'passportExpiry' || registration.passportNumber)
@@ -193,7 +179,7 @@ export function SeafarerRegistrationReviewPage() {
<Text fw={600} size="sm" mb={4}>
Documents
</Text>
<Table withTableBorder withColumnBorders>
<Table variant="vertical">
<Table.Tbody>
{slots.map((slot) => {
const file = attachments.find((a) => a.documentKey === slot.key)?.files?.[0];

View File

@@ -1,27 +1,8 @@
import { StatusBadge } from '@ema-platform/ui';
import { PageHeader, StatusBadge } from '@ema-platform/ui';
import { type StatusTone } from '@ema-platform/shared';
import { useState } from 'react';
import { useApiQuery } from '@ema-platform/api';
import {
ActionIcon,
Badge,
Button,
Card,
Collapse,
Divider,
Group,
Modal,
Paper,
Select,
SimpleGrid,
Stack,
Table,
Text,
TextInput,
ThemeIcon,
Title,
rem,
} from '@mantine/core';
import {ActionIcon, Badge, Button, Card, Collapse, Divider, Group, Modal, Paper, Select, SimpleGrid, Stack, Table, Text, TextInput, ThemeIcon, rem} from '@mantine/core';
import {
IconBook2,
IconCertificate,
@@ -171,10 +152,11 @@ export function SeafarerRegistryPage() {
return (
<Stack gap="md">
<div>
<Title order={3}>Seafarer Registry</Title>
<Text fz="sm" c="dimmed">Search and view all registered seafarers, their documents, and certificate status</Text>
</div>
<PageHeader
title="Seafarer Registry"
subtitle="Search and view all registered seafarers, their documents, and certificate status"
noMargin
/>
{/* KPIs */}
<SimpleGrid cols={{ base: 3, sm: 6 }} spacing="sm">

View File

@@ -336,7 +336,7 @@ export function VesselRegistrationFormBuilderPage() {
<IconSettings size={24} />
</ThemeIcon>
<div>
<Title order={3}>Vessel Registration Form Builder</Title>
<Title order={2}>Vessel Registration Form Builder</Title>
<Text fz="sm" c="dimmed">Add, edit, reorder, or disable fields on the vessel registration form</Text>
</div>
</Group>

View File

@@ -1,29 +1,12 @@
import { useState } from 'react';
import { Link } from 'react-router-dom';
import {
Alert,
Badge,
Button,
Card,
Container,
Drawer,
Group,
Loader,
Modal,
Select,
Stack,
Table,
Text,
TextInput,
Textarea,
Title,
} from '@mantine/core';
import {Alert, Badge, Button, Card, Container, Drawer, Group, Loader, Modal, Select, Stack, Table, Text, TextInput, Textarea} from '@mantine/core';
import {
IconAlertTriangle,
IconInfoCircle,
IconSearch,
} from '@tabler/icons-react';
import { AdvancedTable, notify, useServerTable } from '@ema-platform/ui';
import { AdvancedTable, notify, PageHeader, useServerTable } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
import {
extractErrorMessage,
@@ -238,26 +221,28 @@ export function VesselRegistrationQueuePage() {
return (
<Container size="xl" py="md">
<Group justify="space-between" mb="md">
<div>
<Title order={3}>Vessel register</Title>
<Text size="sm" c="dimmed">
<PageHeader
title="Vessel register"
subtitle={
<>
{data?.total ?? 0} vessel{(data?.total ?? 0) === 1 ? '' : 's'}
pending registrations are reviewed in the{' '}
<Text component={Link} to="/licence-review" inherit c="blue">
licence queue
</Text>
.
</Text>
</div>
<TextInput
placeholder="Name, registration № or IMO"
leftSection={<IconSearch size={14} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
w={280}
/>
</Group>
</>
}
action={
<TextInput
placeholder="Name, registration № or IMO"
leftSection={<IconSearch size={14} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
w={280}
/>
}
/>
<AdvancedTable
tableName="Vessel register"

View File

@@ -1,13 +1,8 @@
import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { Alert, Container, Group, Text, Title } from '@mantine/core';
import {Alert, Container, Group, Text} from '@mantine/core';
import { IconAlertTriangle, IconShip } from '@tabler/icons-react';
import {
ApiErrorAlert,
EmptyState,
PageLoader,
notify,
} from '@ema-platform/ui';
import { ApiErrorAlert, EmptyState, notify, PageHeader, PageLoader } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
import {
downloadAuthedFile,
@@ -85,16 +80,14 @@ export function VesselRegistrationReportPage() {
return (
<Container size="xl" py="md">
<Group justify="space-between" mb="md" align="flex-start">
<div>
<Title order={3}>Vessel registration report</Title>
<Text size="sm" c="dimmed">
{report
? `Register-wide totals with a ${showDate(report.filters.from)} ${showDate(report.filters.to)} window on the trends.`
: 'The national vessel register at a glance.'}
</Text>
</div>
</Group>
<PageHeader
title="Vessel registration report"
subtitle={
report
? `Register-wide totals with a ${showDate(report.filters.from)} ${showDate(report.filters.to)} window on the trends.`
: 'The national vessel register at a glance.'
}
/>
<ReportFilters
query={query}