mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-04 18:23:41 +00:00
112 lines
3.9 KiB
TypeScript
112 lines
3.9 KiB
TypeScript
import { Button, Group, Select, Stack, Text } from '@mantine/core';
|
|
import { IconPlus } from '@tabler/icons-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useLocalized, type LicenseType, type Rank } from '@ema-platform/api';
|
|
import { groupedTypeOptions } from '../config/designer';
|
|
|
|
interface Props {
|
|
licenseTypes: LicenseType[];
|
|
typeId: string | null;
|
|
onTypeChange: (id: string | null) => void;
|
|
/** The selected licence type's rank ladder — empty for non-CoC/CoP types. */
|
|
ranks: Rank[];
|
|
rankId: string | null;
|
|
onRankChange: (id: string | null) => void;
|
|
/** Shown for context only; edited on Certificate Requirements → Behaviour. */
|
|
validityMonths: number;
|
|
/** Non-null when the type is configured in days rather than months. */
|
|
validityDays?: number | null;
|
|
canEdit: boolean;
|
|
onNewVersion: () => void;
|
|
}
|
|
|
|
/** Licence type, certificate validity, and the entry point for a new version. */
|
|
export function DesignerToolbar({
|
|
licenseTypes,
|
|
typeId,
|
|
onTypeChange,
|
|
ranks,
|
|
rankId,
|
|
onRankChange,
|
|
validityMonths,
|
|
validityDays,
|
|
canEdit,
|
|
onNewVersion,
|
|
}: Props) {
|
|
const { t } = useTranslation();
|
|
const localized = useLocalized();
|
|
|
|
return (
|
|
<Group align="flex-end" mb="md" gap="sm">
|
|
{/* Grouped and searchable because the catalogue is 80+ types, over 50 of
|
|
them STCW certificates: a flat list buries the CoC/CoP a user is
|
|
looking for among the logistics operator licences. */}
|
|
<Select
|
|
label={t('designer.licenceType', 'Licence type')}
|
|
data={groupedTypeOptions(licenseTypes, localized, t)}
|
|
value={typeId}
|
|
onChange={onTypeChange}
|
|
searchable
|
|
nothingFoundMessage={t('designer.noTypeMatch', 'No licence type matches')}
|
|
maxDropdownHeight={340}
|
|
w={340}
|
|
/>
|
|
|
|
{/* CoC/CoP only — a rank can carry its own design (e.g. Master's
|
|
certificate differs from an OOW's). "Default" (null) is the design
|
|
every other rank under the type falls back to. */}
|
|
{ranks.length > 0 && (
|
|
<Select
|
|
label={t('designer.rank', 'Rank')}
|
|
description={t('designer.rankHint', 'Leave as Default to design for every rank')}
|
|
data={[
|
|
{ value: '', label: t('designer.rankDefault', 'Default (all ranks)') },
|
|
...ranks.map((r) => ({ value: r.id, label: localized(r.name) })),
|
|
]}
|
|
value={rankId ?? ''}
|
|
onChange={(value) => onRankChange(value || null)}
|
|
w={220}
|
|
/>
|
|
)}
|
|
|
|
{/* Read-only here. Validity is one policy decision with the renewal
|
|
window and the expiry reminders, so it is edited in one place —
|
|
Certificate Requirements → Behaviour — rather than from two screens
|
|
behind two different permissions. Still shown, because a designer
|
|
laying out a certificate that prints an expiry needs to see the term
|
|
it promises. */}
|
|
{typeId && (
|
|
<Stack gap={2}>
|
|
<Text size="xs" c="dimmed" fw={500}>
|
|
{t('designer.validity', 'Valid for')}
|
|
</Text>
|
|
<Group gap={6} align="baseline">
|
|
<Text size="sm" fw={600}>
|
|
{validityDays != null
|
|
? t('designer.validityDays', '{{count}} days', { count: validityDays })
|
|
: validityMonths
|
|
? t('designer.validityMonths', '{{count}} months', {
|
|
count: validityMonths,
|
|
})
|
|
: t('designer.validityNone', 'Does not expire')}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{t('designer.validityEditedOn', '— set on Certificate Requirements → Behaviour')}
|
|
</Text>
|
|
</Group>
|
|
</Stack>
|
|
)}
|
|
|
|
<div style={{ flex: 1 }} />
|
|
|
|
<Button
|
|
leftSection={<IconPlus size={16} />}
|
|
disabled={!canEdit || !typeId}
|
|
onClick={onNewVersion}
|
|
>
|
|
{t('designer.newVersion', 'New version')}
|
|
</Button>
|
|
</Group>
|
|
);
|
|
}
|