mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
ui chnages
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { Badge, Button, Text } from '@mantine/core';
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { AdvancedColumn } from '@ema-platform/ui';
|
||||
import { StatusBadge } from '@ema-platform/ui';
|
||||
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
|
||||
import type { ExamIncident, ExamIncidentStatus } from '../../types/exam';
|
||||
|
||||
const STATUS_COLOR: Record<ExamIncidentStatus, string> = {
|
||||
OPEN: 'red',
|
||||
UNDER_REVIEW: 'yellow',
|
||||
RESOLVED: 'teal',
|
||||
DISMISSED: 'gray',
|
||||
const STATUS_TONE: Record<ExamIncidentStatus, StatusTone> = {
|
||||
OPEN: 'danger',
|
||||
UNDER_REVIEW: 'warning',
|
||||
RESOLVED: 'success',
|
||||
DISMISSED: 'neutral',
|
||||
};
|
||||
|
||||
export function examIncidentColumns(
|
||||
@@ -56,13 +58,12 @@ export function examIncidentColumns(
|
||||
{
|
||||
header: t('exam.incidents.status'),
|
||||
cell: ({ row }) => (
|
||||
<Badge
|
||||
<StatusBadge
|
||||
tone={STATUS_TONE[row.original.status] ?? 'neutral'}
|
||||
label={t(`exam.incidentStatus.${row.original.status}`)}
|
||||
size="sm"
|
||||
variant="light"
|
||||
color={STATUS_COLOR[row.original.status] ?? 'gray'}
|
||||
>
|
||||
{t(`exam.incidentStatus.${row.original.status}`)}
|
||||
</Badge>
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
@@ -39,7 +40,7 @@ import {
|
||||
IconCheck,
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
import { ModalFooter, notify, useErrorHandler } from '@ema-platform/ui';
|
||||
import { StatusBadge, ModalFooter, notify, useErrorHandler } from '@ema-platform/ui';
|
||||
import { extractErrorMessage } from '@ema-platform/api';
|
||||
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
|
||||
import {
|
||||
@@ -57,13 +58,13 @@ import { ExamIncidentsPanel } from '../components/ExamIncidentsPanel';
|
||||
import { PageLoader } from '@ema-platform/ui';
|
||||
import type { ExamStatus, QuestionBrief } from '../types/exam';
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
PENDING: "gray",
|
||||
ACTIVE: "blue",
|
||||
COMPLETED: "teal",
|
||||
CANCELLED: "red",
|
||||
POSTPONED: "orange",
|
||||
PUBLISHED: "green",
|
||||
const STATUS_TONE: Record<string, StatusTone> = {
|
||||
PENDING: 'neutral',
|
||||
ACTIVE: 'info',
|
||||
COMPLETED: 'success',
|
||||
CANCELLED: 'danger',
|
||||
POSTPONED: 'pending',
|
||||
PUBLISHED: 'success',
|
||||
};
|
||||
|
||||
const FORM_LABEL: Record<string, string> = { ESSAY: "Essay", CHOICE: "Choice" };
|
||||
@@ -313,14 +314,13 @@ export function ExamDetailPage() {
|
||||
</Group>
|
||||
|
||||
{/* Status badge */}
|
||||
<Badge
|
||||
<StatusBadge
|
||||
tone={STATUS_TONE[exam.status]}
|
||||
label={t(`exam.status.${exam.status}`)}
|
||||
size="lg"
|
||||
variant="light"
|
||||
color={STATUS_COLOR[exam.status]}
|
||||
style={{ width: "fit-content" }}
|
||||
>
|
||||
{t(`exam.status.${exam.status}`)}
|
||||
</Badge>
|
||||
/>
|
||||
|
||||
{/* Exam Info */}
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { Badge, Text } from "@mantine/core";
|
||||
import type { TFunction } from "i18next";
|
||||
import type { AdvancedColumn } from "@ema-platform/ui";
|
||||
import { StatusBadge } from '@ema-platform/ui';
|
||||
import type { Exam } from "../../types/exam";
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
PENDING: "gray",
|
||||
ACTIVE: "blue",
|
||||
COMPLETED: "teal",
|
||||
CANCELLED: "red",
|
||||
POSTPONED: "orange",
|
||||
PUBLISHED: "green",
|
||||
const STATUS_TONE: Record<string, StatusTone> = {
|
||||
PENDING: 'neutral',
|
||||
ACTIVE: 'info',
|
||||
COMPLETED: 'success',
|
||||
CANCELLED: 'danger',
|
||||
POSTPONED: 'pending',
|
||||
PUBLISHED: 'success',
|
||||
};
|
||||
|
||||
export function examColumns(
|
||||
@@ -72,9 +74,12 @@ export function examColumns(
|
||||
{
|
||||
header: t("exam.columns.status"),
|
||||
cell: ({ row }) => (
|
||||
<Badge size="sm" variant="light" color={STATUS_COLOR[row.original.status]}>
|
||||
{t(`exam.status.${row.original.status}`)}
|
||||
</Badge>
|
||||
<StatusBadge
|
||||
tone={STATUS_TONE[row.original.status]}
|
||||
label={t(`exam.status.${row.original.status}`)}
|
||||
size="sm"
|
||||
variant="light"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Badge } from '@mantine/core';
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import type { AdvancedColumn } from '@ema-platform/ui';
|
||||
import { StatusBadge } from '@ema-platform/ui';
|
||||
import type { Item } from '../../api/item-api';
|
||||
|
||||
const STATUS_COLORS: Record<Item['status'], string> = {
|
||||
DRAFT: 'gray',
|
||||
ACTIVE: 'green',
|
||||
ARCHIVED: 'orange',
|
||||
const STATUS_TONES: Record<Item['status'], StatusTone> = {
|
||||
DRAFT: 'neutral',
|
||||
ACTIVE: 'success',
|
||||
ARCHIVED: 'pending',
|
||||
};
|
||||
|
||||
export function itemColumns(showDate: (date: string) => string): AdvancedColumn<Item>[] {
|
||||
@@ -14,7 +15,7 @@ export function itemColumns(showDate: (date: string) => string): AdvancedColumn<
|
||||
{
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<Badge color={STATUS_COLORS[row.original.status]}>{row.original.status}</Badge>
|
||||
<StatusBadge tone={STATUS_TONES[row.original.status]} label={row.original.status} />
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Badge, Button, Text, Tooltip } from '@mantine/core';
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { Button, Text, Tooltip } from '@mantine/core';
|
||||
import { IconShieldCog } from '@tabler/icons-react';
|
||||
import type { AdvancedColumn } from '@ema-platform/ui';
|
||||
import { StatusBadge } from '@ema-platform/ui';
|
||||
import type { Bilingual, IssuedLicense } from '@ema-platform/api';
|
||||
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
|
||||
|
||||
const LICENSE_STATUS_COLORS: Record<string, string> = {
|
||||
ACTIVE: 'green',
|
||||
EXPIRED: 'yellow',
|
||||
SUSPENDED: 'orange',
|
||||
CANCELLED: 'red',
|
||||
SUPERSEDED: 'gray',
|
||||
const LICENSE_STATUS_TONES: Record<string, StatusTone> = {
|
||||
ACTIVE: 'success',
|
||||
EXPIRED: 'warning',
|
||||
SUSPENDED: 'pending',
|
||||
CANCELLED: 'danger',
|
||||
SUPERSEDED: 'neutral',
|
||||
};
|
||||
|
||||
export type LifecycleAction = 'suspend' | 'revoke' | 'reinstate';
|
||||
@@ -70,13 +72,12 @@ export function licenseRegisterColumns(
|
||||
{
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<Badge
|
||||
<StatusBadge
|
||||
tone={LICENSE_STATUS_TONES[row.original.status] ?? 'neutral'}
|
||||
label={row.original.status}
|
||||
size="sm"
|
||||
variant="light"
|
||||
color={LICENSE_STATUS_COLORS[row.original.status] ?? 'gray'}
|
||||
>
|
||||
{row.original.status}
|
||||
</Badge>
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { Badge, Text } from '@mantine/core';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { AdvancedColumn } from '@ema-platform/ui';
|
||||
import { StatusBadge } from '@ema-platform/ui';
|
||||
import {
|
||||
seaServiceDays,
|
||||
type MedicalCertificate,
|
||||
@@ -18,10 +20,10 @@ export function ownerName(profile?: SeafarerProfileSummary): string {
|
||||
);
|
||||
}
|
||||
|
||||
const STATUS_COLOR: Record<SeafarerRecordStatus, string> = {
|
||||
SUBMITTED: 'yellow',
|
||||
VERIFIED: 'teal',
|
||||
REJECTED: 'red',
|
||||
const STATUS_TONE: Record<SeafarerRecordStatus, StatusTone> = {
|
||||
SUBMITTED: 'warning',
|
||||
VERIFIED: 'success',
|
||||
REJECTED: 'danger',
|
||||
};
|
||||
|
||||
/** Only meaningful now the queue can show ruled records too. */
|
||||
@@ -33,9 +35,12 @@ function statusColumn<T extends { status: SeafarerRecordStatus }>(
|
||||
label: t('recordVerification.columns.status', 'Status'),
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => (
|
||||
<Badge size="sm" variant="light" color={STATUS_COLOR[row.original.status]}>
|
||||
{t(`recordVerification.status.${row.original.status}`, row.original.status)}
|
||||
</Badge>
|
||||
<StatusBadge
|
||||
tone={STATUS_TONE[row.original.status]}
|
||||
label={t(`recordVerification.status.${row.original.status}`, row.original.status)}
|
||||
size="sm"
|
||||
variant="light"
|
||||
/>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Modal,
|
||||
Text,
|
||||
TextInput,
|
||||
Card,
|
||||
Alert,
|
||||
Select,
|
||||
NumberInput,
|
||||
@@ -238,9 +237,12 @@ export function QuestionPage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card withBorder padding={0}>
|
||||
<Group p="md" justify="space-between" wrap="wrap" gap="sm">
|
||||
<Text fw={600}>{t('question.pool')}</Text>
|
||||
<AdvancedTable
|
||||
columns={columns}
|
||||
data={page.rows}
|
||||
title={t('question.pool')}
|
||||
tableName={t('question.title')}
|
||||
toolbar={
|
||||
<Select
|
||||
placeholder={t('question.filterByCertification')}
|
||||
data={[{ value: '', label: 'All' }, ...certOptions]}
|
||||
@@ -250,12 +252,8 @@ export function QuestionPage() {
|
||||
style={{ width: 280 }}
|
||||
clearable
|
||||
/>
|
||||
</Group>
|
||||
<AdvancedTable
|
||||
columns={columns}
|
||||
data={page.rows}
|
||||
tableName={t('question.title')}
|
||||
itemCount={page.itemCount}
|
||||
}
|
||||
itemCount={page.itemCount}
|
||||
pageIndex={page.pageIndex}
|
||||
onPageChange={setPageIndex}
|
||||
pageSize={pageSize}
|
||||
@@ -264,7 +262,6 @@ export function QuestionPage() {
|
||||
isLoading={isFetching}
|
||||
emptyText={t('question.noQuestions')}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
opened={Boolean(reviewTarget)}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { type StatusTone, STATUS_TONE_COLOR } from '@ema-platform/shared';
|
||||
import { Badge, Box, Text } from '@mantine/core';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { AdvancedColumn } from '@ema-platform/ui';
|
||||
import { StatusBadge } from '@ema-platform/ui';
|
||||
import type { Result, ResultReviewStatus } from '../../types/result';
|
||||
|
||||
export const STATUS_COLOR: Record<string, string> = {
|
||||
PASSED: 'teal',
|
||||
FAILED: 'red',
|
||||
export const STATUS_TONE: Record<string, StatusTone> = {
|
||||
PASSED: 'success',
|
||||
FAILED: 'danger',
|
||||
};
|
||||
|
||||
/** Where a mark sits in quality control (US-EXAM-011 → 014). */
|
||||
@@ -46,20 +48,22 @@ export function resultColumns(
|
||||
{
|
||||
header: t('result.columns.status'),
|
||||
cell: ({ row }) => (
|
||||
<Badge
|
||||
<StatusBadge
|
||||
tone={STATUS_TONE[row.original.status]}
|
||||
label={t(`result.status.${row.original.status}`)}
|
||||
size="sm"
|
||||
variant="light"
|
||||
color={STATUS_COLOR[row.original.status]}
|
||||
leftSection={
|
||||
<Box
|
||||
w={6}
|
||||
h={6}
|
||||
style={{ borderRadius: 999, background: `var(--mantine-color-${STATUS_COLOR[row.original.status]}-6)` }}
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
background: `var(--mantine-color-${STATUS_TONE_COLOR[STATUS_TONE[row.original.status]]}-6)`,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t(`result.status.${row.original.status}`)}
|
||||
</Badge>
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
Modal,
|
||||
Text,
|
||||
Paper,
|
||||
Card,
|
||||
Loader,
|
||||
Center,
|
||||
Alert,
|
||||
@@ -34,7 +33,7 @@ import {
|
||||
IconSearch,
|
||||
IconSend,
|
||||
} from '@tabler/icons-react';
|
||||
import { notify, BilingualInput, useErrorHandler, AdvancedTable, useServerTable, ModalFooter } from '@ema-platform/ui';
|
||||
import { notify, BilingualInput, useErrorHandler, AdvancedTable, StatusBadge, useServerTable, ModalFooter } from '@ema-platform/ui';
|
||||
import { useDateDisplayer } from '@ema-platform/shared';
|
||||
import type { BilingualValue } from '@ema-platform/ui';
|
||||
import { extractErrorMessage, useLocalized } from '@ema-platform/api';
|
||||
@@ -53,7 +52,7 @@ import { useGetExamsQuery } from '../../../exam/api/exam-api';
|
||||
import { RecordResultModal } from '../../components/RecordResultModal';
|
||||
import type { Result, ResultBreakdown } from '../../types/result';
|
||||
import type { Exam } from '../../../exam/types/exam';
|
||||
import { resultColumns, STATUS_COLOR, REVIEW_COLOR } from './columns';
|
||||
import { resultColumns, STATUS_TONE, REVIEW_COLOR } from './columns';
|
||||
import { resultActionsColumn, type QcAction } from './actions';
|
||||
|
||||
function ResultStat({
|
||||
@@ -327,10 +326,13 @@ export function ResultPage() {
|
||||
<ResultStat label={t('result.stats.avgScore')} value={avgScore} icon={IconChartBar} color="indigo" />
|
||||
</SimpleGrid>
|
||||
|
||||
<Card withBorder padding={0}>
|
||||
<Group p="md" justify="space-between" wrap="wrap" gap="sm">
|
||||
<Text fw={600}>{t('result.section')}</Text>
|
||||
<Group gap="sm" wrap="wrap">
|
||||
<AdvancedTable
|
||||
columns={columns}
|
||||
data={page.rows}
|
||||
title={t('result.section')}
|
||||
tableName={t('result.title')}
|
||||
toolbar={
|
||||
<>
|
||||
<TextInput
|
||||
placeholder={t('result.search.seafarer')}
|
||||
leftSection={<IconSearch size={15} />}
|
||||
@@ -348,23 +350,17 @@ export function ResultPage() {
|
||||
style={{ width: 280 }}
|
||||
clearable
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<AdvancedTable
|
||||
columns={columns}
|
||||
data={page.rows}
|
||||
tableName={t('result.title')}
|
||||
itemCount={page.itemCount}
|
||||
pageIndex={page.pageIndex}
|
||||
onPageChange={setPageIndex}
|
||||
pageSize={pageSize}
|
||||
onPageSizeChange={setPageSize}
|
||||
refresh={refetch}
|
||||
isLoading={isFetching}
|
||||
emptyText={t('result.noItems')}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
}
|
||||
itemCount={page.itemCount}
|
||||
pageIndex={page.pageIndex}
|
||||
onPageChange={setPageIndex}
|
||||
pageSize={pageSize}
|
||||
onPageSizeChange={setPageSize}
|
||||
refresh={refetch}
|
||||
isLoading={isFetching}
|
||||
emptyText={t('result.noItems')}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
opened={detailOpened}
|
||||
@@ -421,9 +417,10 @@ export function ResultPage() {
|
||||
{t('result.review.derivedStatus')}
|
||||
</Text>
|
||||
<Group gap="xs" mt={4}>
|
||||
<Badge variant="light" color={STATUS_COLOR[detailResult.status]}>
|
||||
{t(`result.status.${detailResult.status}`)}
|
||||
</Badge>
|
||||
<StatusBadge
|
||||
tone={STATUS_TONE[detailResult.status]}
|
||||
label={t(`result.status.${detailResult.status}`)}
|
||||
/>
|
||||
<Badge variant="light" color={REVIEW_COLOR[detailResult.reviewStatus] ?? 'gray'}>
|
||||
{t(`result.review.${detailResult.reviewStatus}`)}
|
||||
</Badge>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Badge, Container, Group, Select, Text, TextInput, Title } from '@mantine/core';
|
||||
import {Badge, Container, Select, Text, TextInput, Title} from '@mantine/core';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import {
|
||||
@@ -113,33 +113,35 @@ export function SeafarerDocumentQueuePage({ kind }: { kind: SeafarerDocumentKind
|
||||
Requests released by an approved seafarer registration: confirm payment, schedule the
|
||||
collection date, then issue.
|
||||
</Text>
|
||||
<Group mb="md" gap="sm">
|
||||
<TextInput
|
||||
placeholder="Search number, name or seafarer №…"
|
||||
leftSection={<IconSearch size={14} />}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.currentTarget.value);
|
||||
setPage(0);
|
||||
}}
|
||||
w={280}
|
||||
/>
|
||||
<Select
|
||||
placeholder="All statuses"
|
||||
data={STATUS_FILTERS}
|
||||
value={status}
|
||||
onChange={(v) => {
|
||||
setStatus(v as SeafarerDocumentStatus | null);
|
||||
setPage(0);
|
||||
}}
|
||||
clearable
|
||||
w={220}
|
||||
/>
|
||||
</Group>
|
||||
<AdvancedTable
|
||||
columns={columns}
|
||||
data={data?.items ?? []}
|
||||
tableName={`${SEAFARER_DOCUMENT_KIND_LABELS[kind]} requests`}
|
||||
toolbar={
|
||||
<>
|
||||
<TextInput
|
||||
placeholder="Search number, name or seafarer №…"
|
||||
leftSection={<IconSearch size={14} />}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.currentTarget.value);
|
||||
setPage(0);
|
||||
}}
|
||||
w={260}
|
||||
/>
|
||||
<Select
|
||||
placeholder="All statuses"
|
||||
data={STATUS_FILTERS}
|
||||
value={status}
|
||||
onChange={(v) => {
|
||||
setStatus(v as SeafarerDocumentStatus | null);
|
||||
setPage(0);
|
||||
}}
|
||||
clearable
|
||||
w={200}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
itemCount={data?.total ?? 0}
|
||||
pageIndex={page}
|
||||
onPageChange={setPage}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Badge, Container, Group, Select, Text, TextInput, Title } from '@mantine/core';
|
||||
import {Container, Select, Text, TextInput, Title} from '@mantine/core';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import {
|
||||
SEAFARER_REGISTRATION_STATUS_COLORS,
|
||||
SEAFARER_REGISTRATION_STATUS_TONES,
|
||||
SEAFARER_REGISTRATION_STATUS_LABELS,
|
||||
displaySeafarerAnswer,
|
||||
useListSeafarerRegistrationsQuery,
|
||||
type SeafarerRegistration,
|
||||
type SeafarerRegistrationStatus,
|
||||
} from '@ema-platform/api';
|
||||
import { AdvancedTable, type AdvancedColumn } from '@ema-platform/ui';
|
||||
import { AdvancedTable, StatusBadge, type AdvancedColumn } from '@ema-platform/ui';
|
||||
import { useDateDisplayer } from '@ema-platform/shared';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
@@ -82,9 +82,10 @@ export function SeafarerRegistrationQueuePage() {
|
||||
header: 'Status',
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => (
|
||||
<Badge size="sm" variant="light" color={SEAFARER_REGISTRATION_STATUS_COLORS[row.original.status]}>
|
||||
{SEAFARER_REGISTRATION_STATUS_LABELS[row.original.status]}
|
||||
</Badge>
|
||||
<StatusBadge
|
||||
tone={SEAFARER_REGISTRATION_STATUS_TONES[row.original.status]}
|
||||
label={SEAFARER_REGISTRATION_STATUS_LABELS[row.original.status]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -100,33 +101,35 @@ export function SeafarerRegistrationQueuePage() {
|
||||
Registrations awaiting review. Approval numbers the seafarer and opens their Seaman Book and
|
||||
BTC applications.
|
||||
</Text>
|
||||
<Group mb="md" gap="sm">
|
||||
<TextInput
|
||||
placeholder="Search number, name or ID…"
|
||||
leftSection={<IconSearch size={14} />}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.currentTarget.value);
|
||||
setPage(0);
|
||||
}}
|
||||
w={280}
|
||||
/>
|
||||
<Select
|
||||
placeholder="All statuses"
|
||||
data={STATUS_FILTERS}
|
||||
value={status}
|
||||
onChange={(v) => {
|
||||
setStatus(v as SeafarerRegistrationStatus | null);
|
||||
setPage(0);
|
||||
}}
|
||||
clearable
|
||||
w={220}
|
||||
/>
|
||||
</Group>
|
||||
<AdvancedTable
|
||||
columns={columns}
|
||||
data={data?.items ?? []}
|
||||
tableName="Seafarer registrations"
|
||||
toolbar={
|
||||
<>
|
||||
<TextInput
|
||||
placeholder="Search number, name or ID…"
|
||||
leftSection={<IconSearch size={14} />}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.currentTarget.value);
|
||||
setPage(0);
|
||||
}}
|
||||
w={260}
|
||||
/>
|
||||
<Select
|
||||
placeholder="All statuses"
|
||||
data={STATUS_FILTERS}
|
||||
value={status}
|
||||
onChange={(v) => {
|
||||
setStatus(v as SeafarerRegistrationStatus | null);
|
||||
setPage(0);
|
||||
}}
|
||||
clearable
|
||||
w={200}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
itemCount={data?.total ?? 0}
|
||||
pageIndex={page}
|
||||
onPageChange={setPage}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
SEAFARER_REGISTRATION_DOCUMENTS,
|
||||
SEAFARER_REGISTRATION_FIELD_LABELS,
|
||||
SEAFARER_REGISTRATION_SECTIONS,
|
||||
SEAFARER_REGISTRATION_STATUS_COLORS,
|
||||
SEAFARER_REGISTRATION_STATUS_TONES,
|
||||
SEAFARER_REGISTRATION_STATUS_LABELS,
|
||||
displaySeafarerAnswer,
|
||||
extractErrorMessage,
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
useRejectSeafarerRegistrationMutation,
|
||||
useRequestSeafarerRegistrationChangesMutation,
|
||||
} from '@ema-platform/api';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { notify, StatusBadge } from '@ema-platform/ui';
|
||||
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
|
||||
import { applicantName } from './SeafarerRegistrationQueuePage';
|
||||
|
||||
@@ -116,9 +116,10 @@ export function SeafarerRegistrationReviewPage() {
|
||||
<Text size="sm" c="dimmed" ff="monospace">
|
||||
{registration.registrationNumber}
|
||||
</Text>
|
||||
<Badge size="sm" variant="light" color={SEAFARER_REGISTRATION_STATUS_COLORS[registration.status]}>
|
||||
{SEAFARER_REGISTRATION_STATUS_LABELS[registration.status]}
|
||||
</Badge>
|
||||
<StatusBadge
|
||||
tone={SEAFARER_REGISTRATION_STATUS_TONES[registration.status]}
|
||||
label={SEAFARER_REGISTRATION_STATUS_LABELS[registration.status]}
|
||||
/>
|
||||
{registration.seafarerNumber && (
|
||||
<Badge size="sm" color="teal" leftSection={<IconCheck size={10} />}>
|
||||
{registration.seafarerNumber}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { StatusBadge } from '@ema-platform/ui';
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { useState } from 'react';
|
||||
import { useApiQuery } from '@ema-platform/api';
|
||||
import {
|
||||
@@ -59,7 +61,7 @@ const STATUS_OPTIONS = ['All', 'Active', 'Inactive', 'Suspended'];
|
||||
const MEDICAL_OPTIONS = ['All', 'Valid', 'Expiring', 'Expired'];
|
||||
|
||||
const MEDICAL_COLOR: Record<string, string> = { Valid: 'teal', Expiring: 'orange', Expired: 'red' };
|
||||
const STATUS_COLOR: Record<string, string> = { Active: 'teal', Inactive: 'gray', Suspended: 'red' };
|
||||
const STATUS_TONE: Record<string, StatusTone> = { Active: 'success', Inactive: 'neutral', Suspended: 'danger' };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Detail modal
|
||||
@@ -83,7 +85,7 @@ function DetailModal({ sf, opened, onClose }: { sf: Seafarer | null; opened: boo
|
||||
<Group justify="space-between"><Text fz="xs" c="dimmed">Nationality</Text><Text fz="xs">{sf.nationality}</Text></Group>
|
||||
<Group justify="space-between"><Text fz="xs" c="dimmed">Date of Birth</Text><Text fz="xs">{sf.dob}</Text></Group>
|
||||
<Group justify="space-between"><Text fz="xs" c="dimmed">Rank</Text><Text fz="xs" fw={600}>{sf.rank}</Text></Group>
|
||||
<Group justify="space-between"><Text fz="xs" c="dimmed">Status</Text><Badge color={STATUS_COLOR[sf.status]} variant="light" size="xs">{sf.status}</Badge></Group>
|
||||
<Group justify="space-between"><Text fz="xs" c="dimmed">Status</Text><StatusBadge tone={STATUS_TONE[sf.status]} label={sf.status} variant="light" size="xs" /></Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
<Paper withBorder radius="md" p="md">
|
||||
@@ -264,7 +266,12 @@ export function SeafarerRegistryPage() {
|
||||
: <Text fz="xs" c="dimmed">—</Text>}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge color={STATUS_COLOR[sf.status]} variant="light" size="xs">{sf.status}</Badge>
|
||||
<StatusBadge
|
||||
tone={STATUS_TONE[sf.status]}
|
||||
label={sf.status}
|
||||
variant="light"
|
||||
size="xs"
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon variant="light" color="blue" size="sm" onClick={() => setSelected(sf)}>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Badge, Button, Text, Tooltip } from '@mantine/core';
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { Button, Text, Tooltip } from '@mantine/core';
|
||||
import { IconShieldCog } from '@tabler/icons-react';
|
||||
import type { AdvancedColumn } from '@ema-platform/ui';
|
||||
import { StatusBadge } from '@ema-platform/ui';
|
||||
import type { Vessel } from '@ema-platform/api';
|
||||
import { LICENSE_PERMISSIONS, RequirePermission } from '@ema-platform/auth';
|
||||
|
||||
const VESSEL_STATUS_COLORS: Record<string, string> = {
|
||||
REGISTERED: 'green',
|
||||
SUSPENDED: 'orange',
|
||||
DEREGISTERED: 'gray',
|
||||
const VESSEL_STATUS_TONES: Record<string, StatusTone> = {
|
||||
REGISTERED: 'success',
|
||||
SUSPENDED: 'pending',
|
||||
DEREGISTERED: 'neutral',
|
||||
};
|
||||
|
||||
export const CATEGORY_LABELS: Record<string, string> = {
|
||||
@@ -63,13 +65,12 @@ export function vesselRegistrationQueueColumns(
|
||||
{
|
||||
header: 'Status',
|
||||
cell: ({ row }) => (
|
||||
<Badge
|
||||
<StatusBadge
|
||||
tone={VESSEL_STATUS_TONES[row.original.status]}
|
||||
label={row.original.status}
|
||||
size="sm"
|
||||
variant="light"
|
||||
color={VESSEL_STATUS_COLORS[row.original.status]}
|
||||
>
|
||||
{row.original.status}
|
||||
</Badge>
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -347,7 +347,7 @@ test.describe('seafarer registration', () => {
|
||||
await expect(page.getByLabel('National ID (Fayda) Number')).toHaveValue('FYD1234567890');
|
||||
await page.getByRole('button', { name: /^continue$/i }).click();
|
||||
|
||||
// Step 2 — Applicant Details.
|
||||
// Step 2 — Details: address and physical characteristics.
|
||||
await page.getByLabel('Place of Birth').fill('Addis Ababa');
|
||||
await pick(page, 'Department', /deck/i);
|
||||
await pick(page, 'City', /addis ababa/i);
|
||||
@@ -356,15 +356,15 @@ test.describe('seafarer registration', () => {
|
||||
await pick(page, 'Eye Colour', /brown/i);
|
||||
await page.getByLabel('Height (cm)').fill('172');
|
||||
await page.getByLabel('Weight (kg)').fill('68');
|
||||
await page.getByLabel('Certificate Number').fill('MED-2026-001');
|
||||
await page.getByLabel('Issuing Clinic or Practitioner').fill('Addis Marine Clinic');
|
||||
await pickDate(page, 'Issue Date', '2026-01-15');
|
||||
await page.getByRole('button', { name: /^continue$/i }).click();
|
||||
|
||||
// Step 3 — Emergency Contact.
|
||||
// Step 3 — Contact & Medical.
|
||||
await page.getByLabel('Full Name').fill('Almaz Tesfaye');
|
||||
await page.getByLabel('Relationship').fill('Sister');
|
||||
await page.getByLabel('Phone Number').fill('+251911222333');
|
||||
await page.getByLabel('Certificate Number').fill('MED-2026-001');
|
||||
await page.getByLabel('Issuing Clinic or Practitioner').fill('Addis Marine Clinic');
|
||||
await pickDate(page, 'Issue Date', '2026-01-15');
|
||||
await page.getByRole('button', { name: /^continue$/i }).click();
|
||||
|
||||
// Step 4 — Documents: all four required slots show as uploaded.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { type StatusTone, STATUS_TONE_COLOR } from '@ema-platform/shared';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useApiQuery } from '@ema-platform/api';
|
||||
import {
|
||||
@@ -30,7 +31,7 @@ import {
|
||||
IconTrash,
|
||||
IconUpload,
|
||||
} from '@tabler/icons-react';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { StatusBadge, notify } from '@ema-platform/ui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -45,11 +46,11 @@ interface BSTRecord {
|
||||
status: 'Valid' | 'Expiring' | 'Expired' | 'Pending Verification';
|
||||
}
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
Valid: 'teal',
|
||||
Expiring: 'orange',
|
||||
Expired: 'red',
|
||||
'Pending Verification': 'yellow',
|
||||
const STATUS_TONE: Record<string, StatusTone> = {
|
||||
Valid: 'success',
|
||||
Expiring: 'pending',
|
||||
Expired: 'danger',
|
||||
'Pending Verification': 'warning',
|
||||
};
|
||||
|
||||
/** Progress across the five STCW A-VI/1 modules, as `/bst/my` reports it. */
|
||||
@@ -289,14 +290,13 @@ export function BasicSafetyTrainingPage() {
|
||||
</Text>
|
||||
</div>
|
||||
{record && (
|
||||
<Badge
|
||||
<StatusBadge
|
||||
tone={STATUS_TONE[record.status]}
|
||||
label={record.status}
|
||||
size="lg"
|
||||
variant="light"
|
||||
color={STATUS_COLOR[record.status]}
|
||||
leftSection={<IconShieldCheck size={14} />}
|
||||
>
|
||||
{record.status}
|
||||
</Badge>
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
@@ -321,7 +321,7 @@ export function BasicSafetyTrainingPage() {
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<Group justify="space-between" mb="md" wrap="wrap" gap="sm">
|
||||
<Group gap="sm">
|
||||
<ThemeIcon size={48} radius="md" color={STATUS_COLOR[record.status]} variant="light">
|
||||
<ThemeIcon size={48} radius="md" color={STATUS_TONE_COLOR[STATUS_TONE[record.status]]} variant="light">
|
||||
<IconShieldCheck size={24} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
@@ -329,9 +329,7 @@ export function BasicSafetyTrainingPage() {
|
||||
<Text fz="xs" c="dimmed">Combined certificate — all 5 STCW components</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Badge color={STATUS_COLOR[record.status]} variant="light">
|
||||
{record.status}
|
||||
</Badge>
|
||||
<StatusBadge tone={STATUS_TONE[record.status]} label={record.status} variant="light" />
|
||||
</Group>
|
||||
|
||||
<Stack gap="xs" mb="md">
|
||||
|
||||
@@ -72,7 +72,7 @@ export function IdentityDetailsStep(
|
||||
);
|
||||
}
|
||||
|
||||
/** Step 2 — Identity, Address, Physical Characteristics, Medical Certificate. */
|
||||
/** Step 2 — Identity, Address and Physical Characteristics. */
|
||||
export function ApplicantDetailsStep(p: StepProps) {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
@@ -138,6 +138,29 @@ export function ApplicantDetailsStep(p: StepProps) {
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 4 — Emergency Contact and Medical Certificate.
|
||||
*
|
||||
* The medical certificate used to sit at the bottom of Applicant Details,
|
||||
* which made that step 17 fields across four sections while this one held
|
||||
* three. Both are short, unrelated-to-identity, and copied off a document in
|
||||
* hand rather than recalled — so they pair here and the wizard's longest step
|
||||
* drops by a third.
|
||||
*/
|
||||
export function EmergencyContactStep(p: StepProps) {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<SectionTitle title="Emergency Contact" description="The person EMA contacts in an emergency." />
|
||||
<Grid>
|
||||
<TextField {...p} name="emergencyContactName" label="Full Name" maxLength={255} />
|
||||
<TextField {...p} name="emergencyContactRelationship" label="Relationship" maxLength={64} />
|
||||
<TextField {...p} name="emergencyContactPhone" label="Phone Number" maxLength={32} />
|
||||
</Grid>
|
||||
|
||||
<Divider />
|
||||
<SectionTitle
|
||||
title="Medical Certificate"
|
||||
@@ -157,17 +180,3 @@ export function ApplicantDetailsStep(p: StepProps) {
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
/** Step 3 — Emergency Contact. */
|
||||
export function EmergencyContactStep(p: StepProps) {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<SectionTitle title="Emergency Contact" description="The person EMA contacts in an emergency." />
|
||||
<Grid>
|
||||
<TextField {...p} name="emergencyContactName" label="Full Name" maxLength={255} />
|
||||
<TextField {...p} name="emergencyContactRelationship" label="Relationship" maxLength={64} />
|
||||
<TextField {...p} name="emergencyContactPhone" label="Phone Number" maxLength={32} />
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Center,
|
||||
Container,
|
||||
@@ -20,7 +19,7 @@ import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
PHYSICAL_BOUNDS,
|
||||
SEAFARER_REGISTRATION_FIELD_LABELS,
|
||||
SEAFARER_REGISTRATION_STATUS_COLORS,
|
||||
SEAFARER_REGISTRATION_STATUS_TONES,
|
||||
SEAFARER_REGISTRATION_STATUS_LABELS,
|
||||
extractErrorMessage,
|
||||
extractValidationIssues,
|
||||
@@ -33,7 +32,7 @@ import {
|
||||
type SeafarerRegistration,
|
||||
type ValidationIssue,
|
||||
} from '@ema-platform/api';
|
||||
import { splitPersonName } from '@ema-platform/ui';
|
||||
import { splitPersonName, StatusBadge } from '@ema-platform/ui';
|
||||
import { useCurrentProfile, type CurrentProfile } from '@ema-platform/auth';
|
||||
import { useAppSelector } from '../../../store/hooks';
|
||||
import { CheckboxField, type AnswerKey } from '../components/fields';
|
||||
@@ -41,16 +40,19 @@ import { ApplicantDetailsStep, EmergencyContactStep, IdentityDetailsStep } from
|
||||
import { RegistrationDocuments, documentSlots } from '../components/RegistrationDocuments';
|
||||
import { RegistrationSummary } from '../components/RegistrationSummary';
|
||||
|
||||
const STEPS = ['Identity Details', 'Applicant Details', 'Emergency Contact', 'Documents', 'Review'];
|
||||
const STEPS = [
|
||||
{ label: 'Identity', description: 'Who you are' },
|
||||
{ label: 'Details', description: 'Address & physical' },
|
||||
{ label: 'Contact & Medical', description: 'Emergency & fitness' },
|
||||
{ label: 'Documents', description: 'Upload evidence' },
|
||||
{ label: 'Review', description: 'Check & submit' },
|
||||
];
|
||||
|
||||
/** Which answers each step must have before "Continue" — mirrors the API's submission check. */
|
||||
const REQUIRED_BY_STEP: AnswerKey[][] = [
|
||||
['firstName', 'lastName', 'gender', 'dateOfBirth', 'maritalStatus', 'nationality', 'nationalIdNumber'],
|
||||
[
|
||||
'placeOfBirth', 'department', 'locationId', 'hairColor', 'eyeColor', 'heightCm', 'weightKg',
|
||||
'medicalCertificateNumber', 'medicalIssuerName', 'medicalIssueDate',
|
||||
],
|
||||
[],
|
||||
['placeOfBirth', 'department', 'locationId', 'hairColor', 'eyeColor', 'heightCm', 'weightKg'],
|
||||
['medicalCertificateNumber', 'medicalIssuerName', 'medicalIssueDate'],
|
||||
[],
|
||||
['declarationAccepted'],
|
||||
];
|
||||
@@ -123,7 +125,7 @@ export function SeafarerRegistrationPage() {
|
||||
const registration = data?.registration ?? null;
|
||||
|
||||
const [start] = useStartSeafarerRegistrationMutation();
|
||||
const [save] = useSaveSeafarerRegistrationMutation();
|
||||
const [save, { isLoading: saving }] = useSaveSeafarerRegistrationMutation();
|
||||
const [submit, { isLoading: submitting }] = useSubmitSeafarerRegistrationMutation();
|
||||
const [startError, setStartError] = useState<string | null>(null);
|
||||
const started = useRef(false);
|
||||
@@ -215,12 +217,16 @@ export function SeafarerRegistrationPage() {
|
||||
}
|
||||
}
|
||||
setErrors(found);
|
||||
const count = Object.keys(found).length;
|
||||
if (count) {
|
||||
const missingKeys = Object.keys(found) as AnswerKey[];
|
||||
if (missingKeys.length) {
|
||||
// Name the fields rather than counting them. "Complete 3 required fields"
|
||||
// sends the applicant hunting up a step they have already scrolled past;
|
||||
// the labels are what let them go straight to it.
|
||||
const names = missingKeys.map((k) => SEAFARER_REGISTRATION_FIELD_LABELS[k]);
|
||||
notifications.show({
|
||||
color: 'red',
|
||||
title: 'Incomplete',
|
||||
message: `Complete ${count} required field${count > 1 ? 's' : ''} to continue.`,
|
||||
title: missingKeys.length > 1 ? 'Some details are missing' : 'One detail is missing',
|
||||
message: `${names.join(', ')}.`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -307,9 +313,10 @@ export function SeafarerRegistrationPage() {
|
||||
<Text size="sm" c="dimmed">
|
||||
{registration.registrationNumber}
|
||||
</Text>
|
||||
<Badge size="sm" variant="light" color={SEAFARER_REGISTRATION_STATUS_COLORS[registration.status]}>
|
||||
{SEAFARER_REGISTRATION_STATUS_LABELS[registration.status]}
|
||||
</Badge>
|
||||
<StatusBadge
|
||||
tone={SEAFARER_REGISTRATION_STATUS_TONES[registration.status]}
|
||||
label={SEAFARER_REGISTRATION_STATUS_LABELS[registration.status]}
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
{showSummary && !readOnly && (
|
||||
@@ -362,8 +369,8 @@ export function SeafarerRegistrationPage() {
|
||||
{!showSummary && (
|
||||
<Paper withBorder p="lg" radius="md">
|
||||
<Stepper active={active} onStepClick={goToStep} size="sm" mb="lg">
|
||||
{STEPS.map((label) => (
|
||||
<Stepper.Step key={label} label={label} />
|
||||
{STEPS.map((step) => (
|
||||
<Stepper.Step key={step.label} label={step.label} description={step.description} />
|
||||
))}
|
||||
</Stepper>
|
||||
|
||||
@@ -408,9 +415,11 @@ export function SeafarerRegistrationPage() {
|
||||
Back
|
||||
</Button>
|
||||
{active < STEPS.length - 1 ? (
|
||||
<Button onClick={() => goToStep(active + 1)}>Continue</Button>
|
||||
<Button loading={saving} onClick={() => goToStep(active + 1)}>
|
||||
Continue
|
||||
</Button>
|
||||
) : (
|
||||
<Button color="teal" loading={submitting} disabled={readOnly} onClick={handleSubmit}>
|
||||
<Button color="teal" loading={saving || submitting} disabled={readOnly} onClick={handleSubmit}>
|
||||
{isAdjusting ? 'Resubmit corrections' : 'Submit registration'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { Badge, Group, Text, Tooltip } from '@mantine/core';
|
||||
import type { TFunction } from 'i18next';
|
||||
import type { AdvancedColumn } from '@ema-platform/ui';
|
||||
import { StatusBadge } from '@ema-platform/ui';
|
||||
import { seaServiceDays, type MedicalCertificate, type SeaServiceRecord } from '@ema-platform/api';
|
||||
|
||||
const RECORD_STATUS_COLORS: Record<string, string> = {
|
||||
SUBMITTED: 'blue',
|
||||
VERIFIED: 'green',
|
||||
REJECTED: 'red',
|
||||
const RECORD_STATUS_TONES: Record<string, StatusTone> = {
|
||||
SUBMITTED: 'info',
|
||||
VERIFIED: 'success',
|
||||
REJECTED: 'danger',
|
||||
};
|
||||
|
||||
export function fitnessOptions(t: TFunction) {
|
||||
@@ -78,11 +80,12 @@ export function seaServiceColumns(
|
||||
label={row.original.verificationRemark ?? ''}
|
||||
disabled={!row.original.verificationRemark}
|
||||
>
|
||||
<Badge color={RECORD_STATUS_COLORS[row.original.status]}>
|
||||
{t(`seaRecords.columns.recordStatus.${row.original.status}`, {
|
||||
<StatusBadge
|
||||
tone={RECORD_STATUS_TONES[row.original.status]}
|
||||
label={t(`seaRecords.columns.recordStatus.${row.original.status}`, {
|
||||
defaultValue: row.original.status,
|
||||
})}
|
||||
</Badge>
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
@@ -140,11 +143,12 @@ export function medicalColumns(
|
||||
label={row.original.verificationRemark ?? ''}
|
||||
disabled={!row.original.verificationRemark}
|
||||
>
|
||||
<Badge color={RECORD_STATUS_COLORS[row.original.status]}>
|
||||
{t(`seaRecords.columns.recordStatus.${row.original.status}`, {
|
||||
<StatusBadge
|
||||
tone={RECORD_STATUS_TONES[row.original.status]}
|
||||
label={t(`seaRecords.columns.recordStatus.${row.original.status}`, {
|
||||
defaultValue: row.original.status,
|
||||
})}
|
||||
</Badge>
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
ActionIcon,
|
||||
@@ -45,7 +46,7 @@ import {
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { StatusBadge, notify } from '@ema-platform/ui';
|
||||
import type { Seafarer } from './SeafarerRegistryPage';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -170,14 +171,14 @@ async function updateSeafarerStatus(_id: string, _status: string): Promise<void>
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
Active: 'teal', Pending: 'yellow', Suspended: 'red',
|
||||
Approved: 'teal', Expired: 'red', Valid: 'teal',
|
||||
Fit: 'teal', Unfit: 'red', Conditional: 'orange',
|
||||
const STATUS_TONE: Record<string, StatusTone> = {
|
||||
Active: 'success', Pending: 'warning', Suspended: 'danger',
|
||||
Approved: 'success', Expired: 'danger', Valid: 'success',
|
||||
Fit: 'success', Unfit: 'danger', Conditional: 'pending',
|
||||
};
|
||||
|
||||
function Chip({ value }: { value: string }) {
|
||||
return <Badge color={STATUS_COLOR[value] ?? 'gray'} variant="light" radius="sm" size="sm">{value}</Badge>;
|
||||
return <StatusBadge tone={STATUS_TONE[value] ?? 'neutral'} label={value} variant="light" radius="sm" size="sm" />;
|
||||
}
|
||||
|
||||
function InfoField({ label, value }: { label: string; value: string }) {
|
||||
@@ -639,7 +640,12 @@ export function SeafarerProfilePage() {
|
||||
</div>
|
||||
</Group>
|
||||
<Stack gap="xs" align="flex-end" style={{ flexShrink: 0 }}>
|
||||
<Badge color={STATUS_COLOR[profile.status] ?? 'gray'} variant="filled" radius="sm">{profile.status}</Badge>
|
||||
<StatusBadge
|
||||
tone={STATUS_TONE[profile.status] ?? 'neutral'}
|
||||
label={profile.status}
|
||||
variant="filled"
|
||||
radius="sm"
|
||||
/>
|
||||
<Button size="xs" variant="default" leftSection={<IconEdit size={13} />} onClick={() => notify.info('Edit profile — coming soon.')}>
|
||||
Edit Profile
|
||||
</Button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { notify } from '@ema-platform/ui';
|
||||
import { StatusBadge, notify } from '@ema-platform/ui';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -163,26 +163,17 @@ function StatCard({
|
||||
// ---------------------------------------------------------------------------
|
||||
// Status badges
|
||||
// ---------------------------------------------------------------------------
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
Active: 'teal',
|
||||
Pending: 'yellow',
|
||||
Suspended: 'red',
|
||||
Expired: 'orange',
|
||||
Fit: 'teal',
|
||||
Unfit: 'red',
|
||||
const STATUS_TONE: Record<string, StatusTone> = {
|
||||
Active: 'success',
|
||||
Pending: 'warning',
|
||||
Suspended: 'danger',
|
||||
Expired: 'pending',
|
||||
Fit: 'success',
|
||||
Unfit: 'danger',
|
||||
};
|
||||
|
||||
function StatusBadge({ value }: { value: string }) {
|
||||
return (
|
||||
<Badge
|
||||
color={STATUS_COLOR[value] ?? 'gray'}
|
||||
variant="light"
|
||||
radius="sm"
|
||||
size="sm"
|
||||
>
|
||||
{value}
|
||||
</Badge>
|
||||
);
|
||||
function RegistryStatus({ value }: { value: string }) {
|
||||
return <StatusBadge tone={STATUS_TONE[value] ?? 'neutral'} label={value} />;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -239,9 +230,9 @@ export function SeafarerRegistryPage() {
|
||||
<Table.Td><Text fz="sm">{s.mobile}</Text></Table.Td>
|
||||
<Table.Td><Text fz="sm">{s.region}</Text></Table.Td>
|
||||
<Table.Td><Text fz="sm">{s.registeredAt}</Text></Table.Td>
|
||||
<Table.Td><StatusBadge value={s.medicalStatus} /></Table.Td>
|
||||
<Table.Td><StatusBadge value={s.bookStatus} /></Table.Td>
|
||||
<Table.Td><StatusBadge value={s.status} /></Table.Td>
|
||||
<Table.Td><RegistryStatus value={s.medicalStatus} /></Table.Td>
|
||||
<Table.Td><RegistryStatus value={s.bookStatus} /></Table.Td>
|
||||
<Table.Td><RegistryStatus value={s.status} /></Table.Td>
|
||||
<Table.Td>
|
||||
<Menu position="bottom-end" shadow="sm" width={160} withinPortal>
|
||||
<Menu.Target>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useApiMutation } from '@ema-platform/api';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
IconTransferIn,
|
||||
IconUser,
|
||||
} from '@tabler/icons-react';
|
||||
import { notify, PhoneInput } from '@ema-platform/ui';
|
||||
import { StatusBadge, notify, PhoneInput } from '@ema-platform/ui';
|
||||
import { isValidPhoneNumber } from 'libphonenumber-js';
|
||||
|
||||
// Minimal vessel type for the approved vessel list
|
||||
@@ -117,11 +117,11 @@ const TRANSFER_REASONS = [
|
||||
'Other',
|
||||
];
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
Pending: 'gray',
|
||||
'Under Review': 'yellow',
|
||||
Approved: 'teal',
|
||||
Rejected: 'red',
|
||||
const STATUS_TONE: Record<string, StatusTone> = {
|
||||
Pending: 'neutral',
|
||||
'Under Review': 'warning',
|
||||
Approved: 'success',
|
||||
Rejected: 'danger',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -140,7 +140,11 @@ function TransferCard({ req }: { req: OwnershipTransferRequest }) {
|
||||
<Text fz="xs" c="dimmed">{req.id} · Transfer to {req.newOwnerName}</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Badge color={STATUS_COLOR[req.status] ?? 'gray'} variant="light">{req.status}</Badge>
|
||||
<StatusBadge
|
||||
tone={STATUS_TONE[req.status] ?? 'neutral'}
|
||||
label={req.status}
|
||||
variant="light"
|
||||
/>
|
||||
</Group>
|
||||
<Divider my="xs" />
|
||||
<SimpleGrid cols={2} spacing="xs">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
IconClockHour4,
|
||||
IconTransferIn,
|
||||
} from '@tabler/icons-react';
|
||||
import { AdvancedTable } from '@ema-platform/ui';
|
||||
import { StatusBadge, AdvancedTable } from '@ema-platform/ui';
|
||||
import { inFlightColumns } from '../inFlightColumns';
|
||||
import {
|
||||
TERMINAL_STATUSES,
|
||||
@@ -68,12 +68,12 @@ interface VesselRegistration {
|
||||
expiryDate: string | null;
|
||||
}
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
Pending: 'gray',
|
||||
'Under Review': 'yellow',
|
||||
Approved: 'teal',
|
||||
Rejected: 'red',
|
||||
'Correction Required': 'orange',
|
||||
const STATUS_TONE: Record<string, StatusTone> = {
|
||||
Pending: 'neutral',
|
||||
'Under Review': 'warning',
|
||||
Approved: 'success',
|
||||
Rejected: 'danger',
|
||||
'Correction Required': 'pending',
|
||||
};
|
||||
|
||||
// Inland vessel certificates (1)
|
||||
@@ -281,9 +281,12 @@ export function VesselRegistrationPage() {
|
||||
<Text fz="xs" c="dimmed">{registration.id}</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Badge color={STATUS_COLOR[registration.status] ?? 'gray'} size="lg" variant="light">
|
||||
{registration.status}
|
||||
</Badge>
|
||||
<StatusBadge
|
||||
tone={STATUS_TONE[registration.status] ?? 'neutral'}
|
||||
label={registration.status}
|
||||
size="lg"
|
||||
variant="light"
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { StatusBadge } from '@ema-platform/ui';
|
||||
import { type StatusTone } from '@ema-platform/shared';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
@@ -30,10 +32,10 @@ import {
|
||||
|
||||
const TRANSFER_TYPE_KEY = 'VESSEL_OWNERSHIP_TRANSFER';
|
||||
|
||||
const VESSEL_STATUS_COLORS: Record<string, string> = {
|
||||
REGISTERED: 'green',
|
||||
SUSPENDED: 'orange',
|
||||
DEREGISTERED: 'gray',
|
||||
const VESSEL_STATUS_TONES: Record<string, StatusTone> = {
|
||||
REGISTERED: 'success',
|
||||
SUSPENDED: 'pending',
|
||||
DEREGISTERED: 'neutral',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -194,12 +196,11 @@ export function VesselTransferPage() {
|
||||
{categoryLabels[vessel.category] ?? vessel.category}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge
|
||||
<StatusBadge
|
||||
tone={VESSEL_STATUS_TONES[vessel.status]}
|
||||
label={vessel.status}
|
||||
size="sm"
|
||||
color={VESSEL_STATUS_COLORS[vessel.status]}
|
||||
>
|
||||
{vessel.status}
|
||||
</Badge>
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Group justify="flex-end">
|
||||
|
||||
@@ -107,12 +107,26 @@ export const SEAFARER_REGISTRATION_STATUS_LABELS: Record<SeafarerRegistrationSta
|
||||
REJECTED: 'Rejected',
|
||||
};
|
||||
|
||||
export const SEAFARER_REGISTRATION_STATUS_COLORS: Record<SeafarerRegistrationStatus, string> = {
|
||||
DRAFT: 'gray',
|
||||
SUBMITTED: 'blue',
|
||||
RESUBMIT_REQUIRED: 'orange',
|
||||
APPROVED: 'teal',
|
||||
REJECTED: 'red',
|
||||
/**
|
||||
* Registration status → platform tone.
|
||||
*
|
||||
* Tones, not colours. `StatusTone` is the platform's status vocabulary and the
|
||||
* one place a tone becomes a colour (`STATUS_TONE_COLOR` in
|
||||
* @ema-platform/shared), so this map cannot drift the way `APPROVED: 'teal'`
|
||||
* had already drifted from every other feature's green success.
|
||||
*
|
||||
* The union is repeated rather than imported because @ema-platform/api does not
|
||||
* depend on the theme layer, and should not start to for five string literals.
|
||||
*/
|
||||
export const SEAFARER_REGISTRATION_STATUS_TONES: Record<
|
||||
SeafarerRegistrationStatus,
|
||||
'success' | 'warning' | 'danger' | 'info' | 'pending' | 'neutral'
|
||||
> = {
|
||||
DRAFT: 'neutral',
|
||||
SUBMITTED: 'info',
|
||||
RESUBMIT_REQUIRED: 'pending',
|
||||
APPROVED: 'success',
|
||||
REJECTED: 'danger',
|
||||
};
|
||||
|
||||
/** Human label for each answer — the review table and the summary both use it. */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import { createTheme, rem } from '@mantine/core';
|
||||
|
||||
/**
|
||||
@@ -97,6 +98,40 @@ export const baseTheme = createTheme({
|
||||
Select: { defaultProps: { radius: 'md' } },
|
||||
PasswordInput: { defaultProps: { radius: 'md' } },
|
||||
|
||||
// The page sits on a tinted surface and cards float on white. Without this
|
||||
// the main area is the same white as every Paper on it, and the card
|
||||
// borders are the only thing separating content from chrome.
|
||||
AppShell: {
|
||||
styles: { main: { background: 'var(--ema-surface-page)' } },
|
||||
},
|
||||
|
||||
// Tables — the registry look: quiet uppercase headers, hairline row
|
||||
// borders, a tint on hover, no zebra striping and no column rules. Set
|
||||
// once here so the 14 pages rendering a raw <Table> match the 28 that go
|
||||
// through AdvancedTable instead of each picking their own density.
|
||||
//
|
||||
// Header text is `text-secondary` rather than the lighter dimmed gray the
|
||||
// mockup used: at 11px uppercase, gray-5 on white fails 4.5:1.
|
||||
Table: {
|
||||
defaultProps: { highlightOnHover: true, verticalSpacing: 'sm', horizontalSpacing: 'md' },
|
||||
styles: {
|
||||
table: {
|
||||
'--table-border-color': 'var(--ema-border-subtle)',
|
||||
'--table-hover-color': 'var(--ema-surface-page)',
|
||||
'--table-striped-color': 'var(--ema-surface-sunken)',
|
||||
} as CSSProperties,
|
||||
th: {
|
||||
fontSize: rem(11),
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
color: 'var(--ema-text-secondary)',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
td: { fontSize: rem(13) },
|
||||
},
|
||||
},
|
||||
|
||||
// Mantine's stock scroll wrapper (NativeScrollArea) discards the
|
||||
// max-height it's handed unless scrollAreaComponent is set, so a modal
|
||||
// taller than the viewport just gets clipped with no way to scroll it.
|
||||
|
||||
@@ -9,6 +9,7 @@ export * from "./lib/feedback/FeatureUnavailable";
|
||||
export * from "./lib/feedback/EmptyState";
|
||||
export * from "./lib/feedback/ErrorState";
|
||||
export * from "./lib/feedback/PageLoader";
|
||||
export * from "./lib/feedback/StatusBadge";
|
||||
export * from "./lib/components/MaritimeLoader";
|
||||
export * from "./lib/theme/maritime-loader-theme";
|
||||
export * from "./lib/layout/AppHeader";
|
||||
|
||||
@@ -51,6 +51,10 @@ interface AdvancedTableProps<T> {
|
||||
rowStyle?: (row: T, index: number) => CSSProperties | undefined;
|
||||
/** Makes rows clickable (adds pointer cursor). */
|
||||
onRowClick?: (row: T) => void;
|
||||
/** Card title, top-left. Defaults to `tableName`, which every caller already passes. */
|
||||
title?: ReactNode;
|
||||
/** Search box, filters, export — rendered top-right before Refresh/View. */
|
||||
toolbar?: ReactNode;
|
||||
}
|
||||
|
||||
function getByPath(obj: unknown, path?: string): unknown {
|
||||
@@ -82,6 +86,8 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
verticalSpacing = "sm",
|
||||
rowStyle,
|
||||
onRowClick,
|
||||
title,
|
||||
toolbar,
|
||||
}: AdvancedTableProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState<boolean[]>(
|
||||
@@ -94,13 +100,22 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
});
|
||||
const shownColumns = columns.filter((_, i) => visible[i] ?? true);
|
||||
|
||||
const heading = title ?? tableName;
|
||||
const from = itemCount === 0 ? 0 : pageIndex * pageSize + 1;
|
||||
const to = Math.min(itemCount, pageIndex * pageSize + data.length);
|
||||
|
||||
return (
|
||||
<Paper withBorder radius="md" p="md">
|
||||
<Group justify="space-between" mb="md">
|
||||
<Paper withBorder radius="lg" p={0}>
|
||||
<Group justify="space-between" px="md" py="sm" wrap="wrap" gap="sm">
|
||||
<Group gap="xs">
|
||||
<Text fw={600}>{""}</Text>
|
||||
{heading && (
|
||||
<Text fw={600} size="sm">
|
||||
{heading}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Group gap="xs" wrap="wrap">
|
||||
{toolbar}
|
||||
{refresh && (
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -162,13 +177,7 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
</Group>
|
||||
|
||||
<Table.ScrollContainer minWidth={480}>
|
||||
<Table
|
||||
striped
|
||||
highlightOnHover
|
||||
withTableBorder
|
||||
withColumnBorders
|
||||
verticalSpacing={verticalSpacing}
|
||||
>
|
||||
<Table verticalSpacing={verticalSpacing}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{shownColumns.map((col, i) => (
|
||||
@@ -230,8 +239,21 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
|
||||
{(itemCount > pageSize || onPageSizeChange) && (
|
||||
<Group justify="flex-end" mt="md">
|
||||
<Group
|
||||
justify="space-between"
|
||||
px="md"
|
||||
py="sm"
|
||||
style={{ borderTop: "1px solid var(--ema-border-subtle)" }}
|
||||
>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("common.showingRange", {
|
||||
from,
|
||||
to,
|
||||
total: itemCount,
|
||||
defaultValue: "Showing {{from}}–{{to}} of {{total}}",
|
||||
})}
|
||||
</Text>
|
||||
<Group gap="sm">
|
||||
{onPageSizeChange && (
|
||||
<Select
|
||||
size="sm"
|
||||
@@ -253,7 +275,7 @@ export function AdvancedTable<T extends { id?: string | number }>({
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
30
libs/ui/src/lib/feedback/StatusBadge.tsx
Normal file
30
libs/ui/src/lib/feedback/StatusBadge.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Badge, type BadgeProps } from '@mantine/core';
|
||||
import { STATUS_TONE_COLOR, type StatusTone } from '@ema-platform/shared';
|
||||
|
||||
export interface StatusBadgeProps extends Omit<BadgeProps, 'color' | 'children'> {
|
||||
/** The platform tone this status maps onto. */
|
||||
tone: StatusTone;
|
||||
/** Human label. Already translated by the caller. */
|
||||
label: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* One badge for every status in the platform.
|
||||
*
|
||||
* There were 29 files rendering `<Badge variant="light" color={MAP[status]}>`,
|
||||
* each re-deciding size, variant and radius alongside the colour — which is why
|
||||
* a "pending" badge in one queue did not match "pending" in the next. Callers
|
||||
* now supply a tone and a label; how a status *looks* is decided once, here.
|
||||
*
|
||||
* Tone rather than colour on purpose: `STATUS_TONE_COLOR` is the single place a
|
||||
* tone becomes a Mantine colour, so restyling the platform's idea of "danger"
|
||||
* stays one edit rather than a sweep.
|
||||
*/
|
||||
export function StatusBadge({ tone, label, ...props }: StatusBadgeProps) {
|
||||
return (
|
||||
<Badge variant="light" size="sm" radius="sm" {...props} color={STATUS_TONE_COLOR[tone]}>
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
@@ -157,8 +157,8 @@ function SidebarItem({ item, collapsed, activePath, onNavigate, opened, onToggle
|
||||
height: rem(40),
|
||||
borderRadius: rem(10),
|
||||
opacity: item.soon ? 0.55 : 1,
|
||||
color: branchActive ? 'var(--mantine-color-blue-6)' : undefined,
|
||||
backgroundColor: branchActive ? 'var(--mantine-color-blue-light)' : undefined,
|
||||
color: branchActive ? 'var(--mantine-primary-color-filled)' : undefined,
|
||||
backgroundColor: branchActive ? 'var(--mantine-primary-color-light)' : undefined,
|
||||
}}
|
||||
>
|
||||
<ItemIcon size={20} stroke={1.6} />
|
||||
@@ -222,7 +222,10 @@ function SidebarItem({ item, collapsed, activePath, onNavigate, opened, onToggle
|
||||
opened={hasChildren ? opened : undefined}
|
||||
onChange={hasChildren ? onToggle : undefined}
|
||||
onClick={() => !hasChildren && onNavigate(item)}
|
||||
variant="light"
|
||||
// A leaf that is the current page is filled in the app's primary; a
|
||||
// branch whose child is current stays a tint, so the filled item is
|
||||
// always exactly the page you are on.
|
||||
variant={hasChildren ? 'light' : 'filled'}
|
||||
styles={{
|
||||
root: { borderRadius: rem(10), opacity: item.soon ? 0.7 : 1 },
|
||||
label: { fontWeight: 500 },
|
||||
|
||||
2014
pnpm-lock.yaml
generated
2014
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user