feat: add document requirements and form schema management for license types

- 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.
This commit is contained in:
Nati
2026-08-20 13:37:26 +00:00
parent 2ec857f0f0
commit 357dc3e4d5
15 changed files with 1845 additions and 0 deletions

View File

@@ -6,6 +6,9 @@ import type {
ApplicationPayment,
ApplicationStaff,
Attachment,
DocumentRequirement,
FormSchemaPalette,
FormSectionConfig,
InitiatePaymentResult,
IssuedLicense,
Inspection,
@@ -25,6 +28,7 @@ import type {
QueueFilter,
RemarkTargetType,
SavedQueueView,
SchemaIssue,
TemplateFieldPlacement,
TemplateLogoPlacement,
TemplatePageOptions,
@@ -66,6 +70,7 @@ const TAGS = [
'License',
'SavedView',
'LicenseTemplate',
'DocumentRequirement',
] as const;
const listTag = (type: (typeof TAGS)[number]) => ({ type, id: 'LIST' }) as const;
@@ -178,6 +183,76 @@ export const licensingApi = baseApi
providesTags: (_r, _e, arg) => [itemTag('LicenseType', arg.idOrKey)],
}),
// ------------------------------------------------ form-schema builder
/** Replaces a licence type's form schema. Server re-validates on save. */
updateFormSchema: builder.mutation<
LicenseType,
{ id: string; formSchema: { sections: FormSectionConfig[] } }
>({
query: ({ id, formSchema }) => ({
url: `/license-types/${id}/form-schema`,
method: 'PUT',
body: { formSchema },
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseType', id), listTag('LicenseType')],
}),
/** Dry-run lint, for inline feedback while the schema is being edited. */
validateFormSchema: builder.mutation<
{ valid: boolean; issues: SchemaIssue[] },
{ formSchema: { sections: FormSectionConfig[] }; licenseTypeId?: string }
>({
query: (body) => ({
url: '/license-types/form-schema/validate',
method: 'POST',
body,
}),
}),
/** Field types, condition operators and prefill sources the builder may offer. */
getFormSchemaPalette: builder.query<FormSchemaPalette, void>({
query: () => ({ url: '/license-types/form-schema/palette' }),
}),
// ------------------------------------------------- document requirements
/**
* Every document requirement, for the admin editor to filter by licence
* type client-side. The collection-query `q` filter syntax (`w=column:op:
* value`) has no typed builder on this side, and the table is small
* configuration data with no pagination need — see `licenseTypeId` usage
* at the call site.
*/
getDocumentRequirements: builder.query<Paginated<DocumentRequirement>, void>({
query: () => ({ url: '/document-requirements' }),
providesTags: () => [listTag('DocumentRequirement')],
}),
createDocumentRequirement: builder.mutation<
DocumentRequirement,
Partial<DocumentRequirement> & { licenseTypeId: string; key: string; name: DocumentRequirement['name'] }
>({
query: (body) => ({ url: '/document-requirements', method: 'POST', body }),
invalidatesTags: (_r, error) => (error ? [] : [listTag('DocumentRequirement')]),
}),
updateDocumentRequirement: builder.mutation<
DocumentRequirement,
{ id: string } & Partial<DocumentRequirement>
>({
query: ({ id, ...body }) => ({
url: `/document-requirements/${id}`,
method: 'PUT',
body,
}),
invalidatesTags: (_r, error) => (error ? [] : [listTag('DocumentRequirement')]),
}),
deleteDocumentRequirement: builder.mutation<unknown, string>({
query: (id) => ({ url: `/document-requirements/${id}`, method: 'DELETE' }),
invalidatesTags: (_r, error) => (error ? [] : [listTag('DocumentRequirement')]),
}),
// -------------------------------------------------------- application
createApplication: builder.mutation<
LicenseApplication,
@@ -827,6 +902,13 @@ export const {
useGetLicenseTypesQuery,
useGetLicenseCategoriesQuery,
useUpdateLicenseFeesMutation,
useUpdateFormSchemaMutation,
useValidateFormSchemaMutation,
useGetFormSchemaPaletteQuery,
useGetDocumentRequirementsQuery,
useCreateDocumentRequirementMutation,
useUpdateDocumentRequirementMutation,
useDeleteDocumentRequirementMutation,
useUpdateLicenseValidityMutation,
useGetLicenseTypeRequirementsQuery,
useCreateApplicationMutation,

View File

@@ -79,10 +79,12 @@ export interface FormFieldConfig {
label: Bilingual;
type: FormFieldType;
required?: boolean;
placeholder?: Bilingual;
helpText?: Bilingual;
options?: { value: string; label: Bilingual }[];
min?: number;
max?: number;
maxLength?: number;
showWhen?: FieldCondition;
readOnly?: boolean;
source?: string;
@@ -235,6 +237,7 @@ export interface StcwCapacityRow {
export interface DocumentRequirement {
id: string;
licenseTypeId: string;
key: string;
name: Bilingual;
description?: Bilingual;
@@ -244,7 +247,32 @@ export interface DocumentRequirement {
allowedMimeTypes: string[];
maxSizeMb: number;
requiresValidityDates: boolean;
allowMultiple: boolean;
sortOrder: number;
isActive: boolean;
}
/**
* What the form-schema builder may put on a form — the field types the engine
* understands, which constraints each one honours, the condition operators it
* supports, and the prefill sources a read-only field may draw from. Drives
* the builder's pickers so they never hardcode a list the server already owns.
*/
export interface FormSchemaPalette {
fieldTypes: {
type: FormFieldType;
supportsOptions: boolean;
supportsRange: boolean;
supportsMaxLength: boolean;
}[];
conditionOperators: ("equals" | "notEquals" | "in" | "isSet")[];
prefillSources: string[];
}
/** One problem the server's form-schema lint found. */
export interface SchemaIssue {
path: string;
message: string;
}
export interface StaffEvidenceRequirement {