Merge pull request #43 from Tria-plc/estif-branch-1

merge from documentVault
This commit is contained in:
Nati Nigussie
2026-08-31 10:23:56 +03:00
committed by GitHub
22 changed files with 2143 additions and 90 deletions

View File

@@ -41,6 +41,8 @@ import type {
TemplateLogoPlacement,
TemplatePageOptions,
TemplateVariable,
PersonalDocumentFilter,
PersonalDocumentGroup,
} from './licensing.types';
/**
@@ -67,6 +69,16 @@ function serialiseQueueFilter(
return params;
}
/** Sends only the facets that are set; `search=` would match nothing. */
function dropEmpty(filter: object): Record<string, unknown> {
const params: Record<string, unknown> = {};
for (const [key, value] of Object.entries(filter)) {
if (value === undefined || value === null || value === '' || value === false) continue;
params[key] = value;
}
return params;
}
const TAGS = [
'LicenseType',
'OperatorType',
@@ -83,6 +95,9 @@ const TAGS = [
'PickupAppointment',
'Department',
'Rank',
// Owned by the personal-document slice; named here so declaring a mode of
// operation can invalidate the vault, whose slots depend on it.
'PersonalDocument',
] as const;
const listTag = (type: (typeof TAGS)[number]) => ({ type, id: 'LIST' }) as const;
@@ -127,9 +142,17 @@ export const licensingApi = baseApi
method: 'PUT',
body,
}),
// The catalogue is filtered by this, so it has to refetch too.
// The catalogue is filtered by this, so it has to refetch too — and so
// is the personal document vault, which asks for the documents the
// declared modes of operation need.
invalidatesTags: (_r, error) =>
error ? [] : [listTag('OperatorType'), listTag('LicenseType')],
error
? []
: [
listTag('OperatorType'),
listTag('LicenseType'),
listTag('PersonalDocument'),
],
}),
/**
@@ -240,9 +263,30 @@ export const licensingApi = baseApi
providesTags: () => [listTag('DocumentRequirement')],
}),
/**
* Personal document slots, grouped by key and paged by the server.
*
* Its own endpoint rather than filtering `getDocumentRequirements` in the
* browser: one document can be configured against several licence types,
* so a page of rows would split a document in half and misreport its
* scope. The server groups first, then pages.
*/
getPersonalDocuments: builder.query<
Paginated<PersonalDocumentGroup>,
PersonalDocumentFilter | void
>({
query: (filter) => ({
url: '/document-requirements/personal',
params: dropEmpty(filter ?? {}),
}),
providesTags: () => [listTag('DocumentRequirement')],
}),
createDocumentRequirement: builder.mutation<
DocumentRequirement,
Partial<DocumentRequirement> & { licenseTypeId: string; key: string; name: DocumentRequirement['name'] }
// No `licenseTypeId` means a personal document, required for every
// licence and served from the applicant's own vault.
Partial<DocumentRequirement> & { key: string; name: DocumentRequirement['name'] }
>({
query: (body) => ({ url: '/document-requirements', method: 'POST', body }),
invalidatesTags: (_r, error) => (error ? [] : [listTag('DocumentRequirement')]),
@@ -1205,6 +1249,7 @@ export const {
useValidateFormSchemaMutation,
useGetFormSchemaPaletteQuery,
useGetDocumentRequirementsQuery,
useGetPersonalDocumentsQuery,
useCreateDocumentRequirementMutation,
useUpdateDocumentRequirementMutation,
useDeleteDocumentRequirementMutation,

View File

@@ -264,7 +264,12 @@ export interface StcwCapacityRow {
export interface DocumentRequirement {
id: string;
licenseTypeId: string;
/**
* Null for a personal document — one every applicant keeps in their own
* vault regardless of what they apply for, rather than a slot on one
* licence's application form.
*/
licenseTypeId: string | null;
key: string;
name: Bilingual;
description?: Bilingual;
@@ -275,6 +280,14 @@ export interface DocumentRequirement {
maxSizeMb: number;
requiresValidityDates: boolean;
allowMultiple: boolean;
/**
* True for a personal document — one the applicant keeps in their own vault
* — rather than an upload slot on an application form. Orthogonal to
* `licenseTypeId`, which still says which licences it applies to.
*/
isPersonal: boolean;
/** Files the slot accepts: 1, 2 for a front and back, null for unlimited. */
maxFiles: number | null;
sortOrder: number;
isActive: boolean;
}
@@ -815,6 +828,31 @@ export interface IssuedLicense {
certificateFileKey: string | null;
}
/**
* One personal document as the backoffice manages it: every configured row
* sharing a key, which is one slot in the applicant's vault. Several rows mean
* the document is scoped to several licence types.
*/
export interface PersonalDocumentGroup {
key: string;
rows: DocumentRequirement[];
}
export interface PersonalDocumentFilter {
/** Matches the key and the name in either locale. */
search?: string;
/** A licence type also matches the documents every licence asks for. */
licenseTypeId?: string;
/** Narrows to the documents configured against no licence type at all. */
globalOnly?: boolean;
sortBy?: 'sortOrder' | 'key' | 'name';
sortDir?: 'ASC' | 'DESC';
take?: number;
skip?: number;
/** Which locale `sortBy: "name"` sorts on. */
locale?: 'en' | 'am';
}
/** One sitting offered to a claimed examined-certificate application, scoped to its rank. */
export interface EligibleExam {
id: string;