mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
- Implement DocumentRequirementsTab for managing document upload requirements. - Create FieldEditorDrawer and SectionEditorDrawer for editing fields and sections in the form schema. - Develop FormSchemaTab to handle sections and fields for a license type's form schema. - Introduce CertificateRequirementsPage to serve as the main interface for configuring license type requirements. - Add hooks for requirement actions to streamline mutation handling and notifications. - Implement condition handling for fields and sections to manage visibility based on user input. - Create schema-paths configuration to facilitate condition target collection.
221 lines
7.7 KiB
TypeScript
221 lines
7.7 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import {
|
|
Button,
|
|
Checkbox,
|
|
Divider,
|
|
Drawer,
|
|
MultiSelect,
|
|
NumberInput,
|
|
Select,
|
|
Stack,
|
|
Text,
|
|
TextInput,
|
|
} from '@mantine/core';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { BilingualInput, ModalFooter } from '@ema-platform/ui';
|
|
import type { ApplicationKind, DocumentRequirement, FormSchemaPalette } from '@ema-platform/api';
|
|
import { ConditionBuilder, type ConditionValue } from './ConditionBuilder';
|
|
import type { ConditionTarget } from '../config/schema-paths';
|
|
|
|
const MIME_OPTIONS = [
|
|
{ value: 'application/pdf', label: 'PDF' },
|
|
{ value: 'image/jpeg', label: 'JPEG' },
|
|
{ value: 'image/png', label: 'PNG' },
|
|
];
|
|
|
|
type DraftRequirement = Omit<DocumentRequirement, 'id' | 'licenseTypeId' | 'isActive'>;
|
|
|
|
function emptyDraft(applicationKind: ApplicationKind): DraftRequirement {
|
|
return {
|
|
key: '',
|
|
name: { en: '', am: '' },
|
|
applicationKind,
|
|
mode: 'ALWAYS',
|
|
allowedMimeTypes: ['application/pdf', 'image/jpeg', 'image/png'],
|
|
maxSizeMb: 5,
|
|
requiresValidityDates: false,
|
|
allowMultiple: false,
|
|
sortOrder: 0,
|
|
};
|
|
}
|
|
|
|
/** Adds/edits one document requirement slot for a licence type + application kind. */
|
|
export function DocumentRequirementEditorDrawer({
|
|
opened,
|
|
onClose,
|
|
requirement,
|
|
defaultApplicationKind,
|
|
onSave,
|
|
palette,
|
|
conditionTargets,
|
|
saving,
|
|
}: {
|
|
opened: boolean;
|
|
onClose: () => void;
|
|
/** Null = adding a new requirement. */
|
|
requirement: DocumentRequirement | null;
|
|
defaultApplicationKind: ApplicationKind;
|
|
onSave: (draft: DraftRequirement) => void;
|
|
palette: FormSchemaPalette | undefined;
|
|
conditionTargets: ConditionTarget[];
|
|
saving: boolean;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const [draft, setDraft] = useState<DraftRequirement>(emptyDraft(defaultApplicationKind));
|
|
const [keyError, setKeyError] = useState<string | null>(null);
|
|
const isNew = !requirement;
|
|
|
|
useEffect(() => {
|
|
if (opened) {
|
|
setDraft(
|
|
requirement
|
|
? {
|
|
key: requirement.key,
|
|
name: { ...requirement.name },
|
|
description: requirement.description ? { ...requirement.description } : undefined,
|
|
applicationKind: requirement.applicationKind,
|
|
mode: requirement.mode,
|
|
conditionExpression: requirement.conditionExpression,
|
|
allowedMimeTypes: requirement.allowedMimeTypes,
|
|
maxSizeMb: requirement.maxSizeMb,
|
|
requiresValidityDates: requirement.requiresValidityDates,
|
|
allowMultiple: requirement.allowMultiple,
|
|
sortOrder: requirement.sortOrder,
|
|
}
|
|
: emptyDraft(defaultApplicationKind),
|
|
);
|
|
setKeyError(null);
|
|
}
|
|
}, [opened, requirement, defaultApplicationKind]);
|
|
|
|
function save() {
|
|
if (!draft.key.trim()) {
|
|
setKeyError(t('certReq.doc.keyRequired', 'Key is required'));
|
|
return;
|
|
}
|
|
if (!draft.name.en?.trim()) return;
|
|
if (draft.mode === 'CONDITIONAL' && !draft.conditionExpression?.field) {
|
|
setKeyError(t('certReq.doc.conditionRequired', 'A conditional requirement needs a condition'));
|
|
return;
|
|
}
|
|
onSave({
|
|
...draft,
|
|
key: draft.key.trim(),
|
|
conditionExpression: draft.mode === 'CONDITIONAL' ? draft.conditionExpression : undefined,
|
|
});
|
|
}
|
|
|
|
return (
|
|
<Drawer
|
|
opened={opened}
|
|
onClose={onClose}
|
|
position="right"
|
|
size="md"
|
|
title={<Text fw={700}>{isNew ? t('certReq.doc.add', 'Add document requirement') : t('certReq.doc.edit', 'Edit document requirement')}</Text>}
|
|
>
|
|
<Stack gap="md">
|
|
<TextInput
|
|
label={t('certReq.doc.key', 'Key')}
|
|
placeholder="bank_letter"
|
|
required
|
|
value={draft.key}
|
|
error={keyError}
|
|
disabled={!isNew}
|
|
description={isNew ? t('certReq.doc.keyHelp', 'Stable slug identifying this document slot') : t('certReq.doc.keyLocked', 'Key cannot change once created')}
|
|
onChange={(e) => setDraft((d) => ({ ...d, key: e.currentTarget.value }))}
|
|
/>
|
|
|
|
<BilingualInput
|
|
label={t('certReq.doc.name', 'Name')}
|
|
required
|
|
value={{ en: draft.name.en ?? '', am: draft.name.am ?? '' }}
|
|
onChange={(v) => setDraft((d) => ({ ...d, name: v }))}
|
|
/>
|
|
|
|
<BilingualInput
|
|
label={t('certReq.doc.description', 'Description')}
|
|
value={{ en: draft.description?.en ?? '', am: draft.description?.am ?? '' }}
|
|
onChange={(v) => setDraft((d) => ({ ...d, description: v }))}
|
|
/>
|
|
|
|
<Select
|
|
label={t('certReq.doc.applicationKind', 'Application kind')}
|
|
data={[
|
|
{ value: 'NEW', label: t('certReq.doc.kindNew', 'New application') },
|
|
{ value: 'RENEWAL', label: t('certReq.doc.kindRenewal', 'Renewal') },
|
|
]}
|
|
value={draft.applicationKind}
|
|
onChange={(v) => v && setDraft((d) => ({ ...d, applicationKind: v as ApplicationKind }))}
|
|
allowDeselect={false}
|
|
disabled={!isNew}
|
|
description={!isNew ? t('certReq.doc.kindLocked', 'Application kind cannot change once created') : undefined}
|
|
/>
|
|
|
|
<Select
|
|
label={t('certReq.doc.mode', 'Mode')}
|
|
data={[
|
|
{ value: 'ALWAYS', label: t('certReq.doc.modeAlways', 'Always required') },
|
|
{ value: 'CONDITIONAL', label: t('certReq.doc.modeConditional', 'Required when condition holds') },
|
|
{ value: 'OPTIONAL', label: t('certReq.doc.modeOptional', 'Optional upload') },
|
|
]}
|
|
value={draft.mode}
|
|
onChange={(v) => v && setDraft((d) => ({ ...d, mode: v as DraftRequirement['mode'] }))}
|
|
allowDeselect={false}
|
|
/>
|
|
|
|
{draft.mode === 'CONDITIONAL' && (
|
|
<>
|
|
<Divider label={t('certReq.condition.title', 'Condition')} labelPosition="left" />
|
|
<ConditionBuilder
|
|
value={(draft.conditionExpression ?? null) as ConditionValue | null}
|
|
onChange={(v) => setDraft((d) => ({ ...d, conditionExpression: v ?? undefined }))}
|
|
targets={conditionTargets}
|
|
palette={palette}
|
|
allowClear={false}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<MultiSelect
|
|
label={t('certReq.doc.allowedTypes', 'Allowed file types')}
|
|
data={MIME_OPTIONS}
|
|
value={draft.allowedMimeTypes}
|
|
onChange={(v) => setDraft((d) => ({ ...d, allowedMimeTypes: v }))}
|
|
/>
|
|
|
|
<NumberInput
|
|
label={t('certReq.doc.maxSize', 'Max file size (MB)')}
|
|
min={1}
|
|
value={draft.maxSizeMb}
|
|
onChange={(v) => setDraft((d) => ({ ...d, maxSizeMb: typeof v === 'number' ? v : d.maxSizeMb }))}
|
|
/>
|
|
|
|
<Checkbox
|
|
label={t('certReq.doc.requiresValidity', 'Requires validity dates')}
|
|
checked={draft.requiresValidityDates}
|
|
onChange={(e) => setDraft((d) => ({ ...d, requiresValidityDates: e.currentTarget.checked }))}
|
|
/>
|
|
|
|
<Checkbox
|
|
label={t('certReq.doc.allowMultiple', 'Allow multiple uploads')}
|
|
checked={draft.allowMultiple}
|
|
onChange={(e) => setDraft((d) => ({ ...d, allowMultiple: e.currentTarget.checked }))}
|
|
/>
|
|
|
|
<NumberInput
|
|
label={t('certReq.doc.sortOrder', 'Sort order')}
|
|
value={draft.sortOrder}
|
|
onChange={(v) => setDraft((d) => ({ ...d, sortOrder: typeof v === 'number' ? v : d.sortOrder }))}
|
|
/>
|
|
|
|
<ModalFooter>
|
|
<Button variant="default" onClick={onClose}>{t('certReq.cancel', 'Cancel')}</Button>
|
|
<Button color="teal" loading={saving} onClick={save}>
|
|
{isNew ? t('certReq.doc.add', 'Add document requirement') : t('certReq.saveChanges', 'Save changes')}
|
|
</Button>
|
|
</ModalFooter>
|
|
</Stack>
|
|
</Drawer>
|
|
);
|
|
}
|