Files
emaui/apps/backoffice/src/app/features/certificate-designer/components/DesignerToolbar.tsx

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>
);
}