Files
emaui/apps/backoffice/src/app/features/certificate-requirements/components/DocumentRequirementEditorDrawer.tsx

230 lines
8.0 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;
// A condition is either a single-field check or an anyOf list — the CoP
// watch_rating_certificate requirement is seeded with anyOf and no field.
const hasCondition =
Boolean(draft.conditionExpression?.field) ||
Boolean(draft.conditionExpression?.anyOf?.length);
if (
draft.mode === 'CONDITIONAL' &&
!hasCondition &&
!draft.conditionExpression?.anyOf?.length
) {
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 ?? { field: '', equals: '' }) as ConditionValue}
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>
);
}