mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
Add rank selection and condition handling to certificate designer and requirements
This commit is contained in:
@@ -1,13 +1,17 @@
|
||||
import { Button, Group, NumberInput, Select, Tooltip } from '@mantine/core';
|
||||
import { IconPlus } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocalized, type LicenseType } from '@ema-platform/api';
|
||||
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;
|
||||
validityMonths: number;
|
||||
onValidityChange: (months: number) => void;
|
||||
currentValidityMonths?: number | null;
|
||||
@@ -22,6 +26,9 @@ export function DesignerToolbar({
|
||||
licenseTypes,
|
||||
typeId,
|
||||
onTypeChange,
|
||||
ranks,
|
||||
rankId,
|
||||
onRankChange,
|
||||
validityMonths,
|
||||
onValidityChange,
|
||||
currentValidityMonths,
|
||||
@@ -49,6 +56,23 @@ export function DesignerToolbar({
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Validity lives beside the design because it is the other half of
|
||||
what a certificate promises. */}
|
||||
<NumberInput
|
||||
|
||||
@@ -23,9 +23,11 @@ import {
|
||||
useArchiveLicenseTemplateMutation,
|
||||
useCreateLicenseTemplateMutation,
|
||||
useDeleteLicenseTemplateMutation,
|
||||
useGetActiveDepartmentsQuery,
|
||||
useGetBuiltInTemplateQuery,
|
||||
useGetLicenseTemplatesQuery,
|
||||
useGetLicenseTypesQuery,
|
||||
useGetRankLadderQuery,
|
||||
useGetTemplateVariablesQuery,
|
||||
usePublishLicenseTemplateMutation,
|
||||
useUpdateLicenseValidityMutation,
|
||||
@@ -66,14 +68,19 @@ export function CertificateDesignerPage() {
|
||||
|
||||
const { data: licenseTypes } = useGetLicenseTypesQuery();
|
||||
const [typeId, setTypeId] = useState<string | null>(null);
|
||||
const [rankId, setRankId] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
data: templates = [],
|
||||
data: allTemplates = [],
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
} = useGetLicenseTemplatesQuery(typeId ?? undefined, { skip: !typeId });
|
||||
// The list is per licence type; a rank-specific design and the type's
|
||||
// default both come back, so the version list is scoped to whichever the
|
||||
// toolbar has selected.
|
||||
const templates = allTemplates.filter((tpl) => (tpl.rankId ?? null) === rankId);
|
||||
const { data: variables = [] } = useGetTemplateVariablesQuery();
|
||||
const { data: builtIn } = useGetBuiltInTemplateQuery();
|
||||
|
||||
@@ -95,6 +102,20 @@ export function CertificateDesignerPage() {
|
||||
|
||||
const selectedType = licenseTypes?.items?.find((type) => type.id === typeId);
|
||||
|
||||
// A rank ladder only exists for CoC/CoP — every other licence type designs
|
||||
// one certificate for everyone who holds it.
|
||||
const isRankScoped =
|
||||
selectedType?.certificateCategory === 'COC' || selectedType?.certificateCategory === 'COP';
|
||||
const { data: departments } = useGetActiveDepartmentsQuery(undefined, { skip: !isRankScoped });
|
||||
const department = departments?.find((d) => d.code === selectedType?.stcwDepartment);
|
||||
const { data: ranks = [] } = useGetRankLadderQuery(
|
||||
{
|
||||
departmentId: department?.id ?? '',
|
||||
certificateCategory: (selectedType?.certificateCategory ?? 'COC') as 'COC' | 'COP',
|
||||
},
|
||||
{ skip: !isRankScoped || !department },
|
||||
);
|
||||
|
||||
// Default to the first licence type so the page is never an empty shell.
|
||||
useEffect(() => {
|
||||
if (!typeId && licenseTypes?.items?.length) setTypeId(licenseTypes.items[0].id);
|
||||
@@ -104,6 +125,12 @@ export function CertificateDesignerPage() {
|
||||
if (selectedType) setValidityMonths(selectedType.validityMonths ?? 12);
|
||||
}, [selectedType]);
|
||||
|
||||
// Switching licence type leaves a stale rank selected from the previous
|
||||
// type's ladder — reset to the type's default design.
|
||||
useEffect(() => {
|
||||
setRankId(null);
|
||||
}, [typeId]);
|
||||
|
||||
function startNewVersion() {
|
||||
setNewName(
|
||||
`${selectedType?.name?.en ?? 'Certificate'} v${(templates[0]?.version ?? 0) + 1}`,
|
||||
@@ -130,6 +157,12 @@ export function CertificateDesignerPage() {
|
||||
setTypeId(value);
|
||||
draft.setSelectedId(null);
|
||||
}}
|
||||
ranks={ranks}
|
||||
rankId={rankId}
|
||||
onRankChange={(value) => {
|
||||
setRankId(value);
|
||||
draft.setSelectedId(null);
|
||||
}}
|
||||
validityMonths={validityMonths}
|
||||
onValidityChange={setValidityMonths}
|
||||
currentValidityMonths={selectedType?.validityMonths}
|
||||
@@ -369,6 +402,7 @@ export function CertificateDesignerPage() {
|
||||
run(async () => {
|
||||
const created = await createTemplate({
|
||||
licenseTypeId: typeId as string,
|
||||
rankId,
|
||||
name: newName.trim(),
|
||||
hbsSource: templates.length ? undefined : builtIn?.hbsSource,
|
||||
}).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user