Merge pull request #64 from Tria-plc/createNewLicenseType

feat: add license type management and import personal document requirements
This commit is contained in:
Estifanos Terefe
2026-09-04 11:47:32 +03:00
committed by GitHub
16 changed files with 1548 additions and 60 deletions

View File

@@ -10,7 +10,7 @@ import { resolveSessionContext } from "../session";
export const BASE_API_URL =
(import.meta as { env?: Record<string, string> }).env?.[
"VITE_BASE_API_URL"
]?.trim() || "http://localhost:3000/api";
]?.trim() || "https://ema-api-dev.triaplc.com/api";
let _onTokenExpired: (() => Promise<string>) | null = null;
let _onAuthFailure: (() => void) | null = null;

View File

@@ -74,6 +74,51 @@ const handlers: MockHandler[] = [
pattern: /^\/license-types$/,
respond: () => paginated(clone(mockLicenseTypes)),
},
{
method: 'POST',
pattern: /^\/license-types$/,
respond: (req) => {
const body = (req.body ?? {}) as Record<string, unknown>;
const key = String(body.key ?? '').trim().toUpperCase();
if (mockLicenseTypes.some((t) => t.key === key)) return undefined;
const created = {
id: `lt-${key.toLowerCase()}`,
category: 'MARITIME_PERSONNEL',
familyKind: 'LOGISTICS_LICENSE',
serviceKind: 'LICENSE',
workflowProfile: 'STANDARD',
feeNewApplication: null,
feeRenewal: null,
feeCurrency: 'ETB',
capitalThreshold: null,
validityMonths: 12,
slaHours: null,
inspectionRequired: true,
issuesCertificate: true,
renewalEnabled: true,
requiresIssuanceScheduling: false,
requiresOperatorMode: true,
formSchema: { sections: [] },
isActive: true,
sortOrder: mockLicenseTypes.length,
...body,
key,
};
(mockLicenseTypes as unknown[]).push(created);
return clone(created);
},
},
{
method: 'PATCH',
pattern: /^\/license-types\/([\w-]+)\/status$/,
respond: (req, match) => {
const found = mockLicenseTypes.find((t) => t.id === match[1]);
if (!found) return undefined;
const body = (req.body ?? {}) as { isActive?: boolean };
(found as { isActive: boolean }).isActive = body.isActive ?? found.isActive;
return clone(found);
},
},
{
method: 'GET',
pattern: /^\/license-types\/requirements\/([\w-]+)$/,

View File

@@ -11,6 +11,7 @@ import type {
Attachment,
Department,
DocumentRequirement,
FamilyKind,
FormSchemaPalette,
FormSectionConfig,
InitiatePaymentResult,
@@ -21,6 +22,7 @@ import type {
LicenseCategoryDefinition,
LicenseStatus,
LicenseType,
LicenseTypeCreate,
LicenseTypeRequirements,
OperatorType,
AssignableOfficer,
@@ -175,6 +177,38 @@ export const licensingApi = baseApi
providesTags: () => [listTag('LicenseType')],
}),
/**
* Creates the licence type row.
*
* The one write on this resource with no screen until now: everything
* after creation — form, documents, staff rules, fees, validity,
* certificate design — had an editor, but the type itself had to be
* added by seed or by calling the API directly.
*/
createLicenseType: builder.mutation<LicenseType, LicenseTypeCreate>({
query: (body) => ({ url: '/license-types', method: 'POST', body }),
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseType')]),
}),
/**
* Replaces the row's own fields. The narrow routes (`/fees`,
* `/validity`, `/behavior`, `/form-schema`) are still the way to change
* what they own; this is the catalogue entry itself — key, names,
* category, family kind, prefix, sort order, active.
*/
updateLicenseType: builder.mutation<
LicenseType,
{ id: string } & Partial<LicenseTypeCreate>
>({
query: ({ id, ...body }) => ({
url: `/license-types/${id}`,
method: 'PUT',
body,
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [listTag('LicenseType'), itemTag('LicenseType', id)],
}),
/**
* Fee configuration, for licensing administrators.
*
@@ -224,6 +258,7 @@ export const licensingApi = baseApi
id: string;
workflowProfile?: WorkflowProfile;
serviceKind?: ServiceKind;
familyKind?: FamilyKind;
completionEffect?: CompletionEffect | null;
certificateCategory?: CertificateCategory | null;
requiresExamination?: boolean;
@@ -254,6 +289,24 @@ export const licensingApi = baseApi
error ? [] : [listTag('LicenseType'), itemTag('LicenseType', id)],
}),
/**
* Opens or closes a type to new applications. Applications already in
* flight hold the type by id and keep running; only the portal
* catalogue changes.
*/
updateLicenseStatus: builder.mutation<
LicenseType,
{ id: string; isActive: boolean }
>({
query: ({ id, isActive }) => ({
url: `/license-types/${id}/status`,
method: 'PATCH',
body: { isActive },
}),
invalidatesTags: (_r, error, { id }) =>
error ? [] : [itemTag('LicenseType', id), listTag('LicenseType')],
}),
/** Validity is edited beside the certificate design, not with the fees. */
updateLicenseValidity: builder.mutation<
LicenseType,
@@ -1382,6 +1435,8 @@ export const licensingApi = baseApi
export const {
useGetLicenseTypesQuery,
useGetLicenseCategoriesQuery,
useCreateLicenseTypeMutation,
useUpdateLicenseTypeMutation,
useUpdateLicenseFeesMutation,
useUpdateLicenseBehaviorMutation,
useUpdateFormSchemaMutation,
@@ -1407,6 +1462,7 @@ export const {
useUpdateRankMutation,
useDeleteRankMutation,
useUpdateLicenseValidityMutation,
useUpdateLicenseStatusMutation,
useGetLicenseTypeRequirementsQuery,
useCreateApplicationMutation,
useGetExamStateForApplicationQuery,

View File

@@ -411,6 +411,18 @@ const ERROR_MESSAGES: Record<string, string> = {
'Candidates are currently waiting to pay this fee. Removing it would leave them unable to pay and unable to continue — set a different amount instead.',
form_schema_missing_protected_paths:
'The form for this licence type does not contain the field this setting depends on. Add the field to the form first.',
license_type_key_taken:
'A licence type with this key already exists (it may be archived). Choose a different key.',
license_type_key_in_use:
'Applications already reference this licence type, so its key can no longer be changed.',
invalid_form_schema:
'The form schema is not consistent — a condition or field reference points at something the form does not define.',
// Exam certification catalogue.
rank_not_found:
'That STCW rank is not in the ranks registry, or has been switched off. Pick a rank from the list.',
certification_in_use:
'Exam sessions or question-bank items still use this certification. Reassign or remove those first.',
};
export function extractErrorMessage(error: unknown, fallback = 'Something went wrong'): string {

View File

@@ -277,6 +277,39 @@ export interface LicenseType {
prerequisiteLicenseKeys?: string[] | null;
}
/**
* Body of `POST /license-types` — the row itself, before behaviour, fees, the
* form or its document slots are configured.
*
* Mirrors `CreateLicenseTypeDto` field for field. Create the type inactive: an
* active type with no form schema and no document requirements is immediately
* visible to applicants.
*/
export interface LicenseTypeCreate {
key: string;
name: Bilingual;
description?: Bilingual;
category?: LicenseCategory;
/** Omitted, the server derives it from `key` — see `FamilyKind`. */
familyKind?: FamilyKind;
/** Licence or registration; splits the catalogues without splitting storage. */
serviceKind?: ServiceKind;
/** Which transition set the application runs. */
workflowProfile?: WorkflowProfile;
certificatePrefix: string;
feeNewApplication?: number | null;
feeRenewal?: number | null;
feeCurrency?: string;
capitalThreshold?: number | null;
validityMonths?: number;
inspectionRequired?: boolean;
issuesCertificate?: boolean;
renewalEnabled?: boolean;
formSchema?: { sections: FormSectionConfig[] };
sortOrder?: number;
isActive?: boolean;
}
/** What kind of document a certificate type produces. */
export type CertificateCategory =
"COC" | "COP" | "ENDORSEMENT" | "GOC" | "NATIONAL";