mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 14:15:45 +00:00
Merge remote-tracking branch 'origin/dev' into fix/coc-exam-workflow-defects
# Conflicts: # apps/portal/src/app/features/licensing/pages/MyApplicationsPage/index.tsx # libs/api/src/lib/features/licensing/licensing-api.ts # libs/api/src/lib/features/licensing/licensing.types.ts
This commit is contained in:
@@ -14,6 +14,7 @@ import type {
|
||||
InitiatePaymentResult,
|
||||
IssuedLicense,
|
||||
Inspection,
|
||||
IssuancePeriod,
|
||||
LicenseApplication,
|
||||
LicenseCategoryDefinition,
|
||||
LicenseStatus,
|
||||
@@ -21,11 +22,16 @@ import type {
|
||||
LicenseTypeRequirements,
|
||||
OperatorType,
|
||||
AssignableOfficer,
|
||||
CertificateCategory,
|
||||
CompletionEffect,
|
||||
DocumentDecision,
|
||||
DocumentReview,
|
||||
ExportResult,
|
||||
LicenseTemplate,
|
||||
Paginated,
|
||||
PickupAppointment,
|
||||
PickupOffice,
|
||||
PickupSlot,
|
||||
QueueCounts,
|
||||
QueueFilter,
|
||||
Rank,
|
||||
@@ -33,10 +39,14 @@ import type {
|
||||
RemarkTargetType,
|
||||
SavedQueueView,
|
||||
SchemaIssue,
|
||||
ServiceKind,
|
||||
TemplateFieldPlacement,
|
||||
TemplateLogoPlacement,
|
||||
TemplatePageOptions,
|
||||
TemplateVariable,
|
||||
PersonalDocumentFilter,
|
||||
PersonalDocumentGroup,
|
||||
WorkflowProfile,
|
||||
} from './licensing.types';
|
||||
|
||||
/**
|
||||
@@ -63,6 +73,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',
|
||||
@@ -75,8 +95,13 @@ const TAGS = [
|
||||
'SavedView',
|
||||
'LicenseTemplate',
|
||||
'DocumentRequirement',
|
||||
'PickupOffice',
|
||||
'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;
|
||||
@@ -121,9 +146,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'),
|
||||
],
|
||||
}),
|
||||
|
||||
/**
|
||||
@@ -153,6 +186,12 @@ export const licensingApi = baseApi
|
||||
feeNewApplication?: number | null;
|
||||
feeRenewal?: number | null;
|
||||
feeCurrency?: string;
|
||||
// The examined-certificate stages. Unlike the two above, these are
|
||||
// read live off the licence type rather than snapshotted, so the
|
||||
// server refuses to clear one a candidate is currently waiting on.
|
||||
feeEligibility?: number | null;
|
||||
feeExamination?: number | null;
|
||||
feeCertificate?: number | null;
|
||||
}
|
||||
>({
|
||||
query: ({ id, ...body }) => ({
|
||||
@@ -164,6 +203,53 @@ export const licensingApi = baseApi
|
||||
error ? [] : [listTag('LicenseType'), itemTag('LicenseType', id)],
|
||||
}),
|
||||
|
||||
/**
|
||||
* How a licence type behaves: its workflow, eligibility gates, renewal
|
||||
* policy and applicant rules — everything that used to be settable only
|
||||
* by editing a seed file.
|
||||
*
|
||||
* Three of these (`workflowProfile`, `completionEffect`,
|
||||
* `requiresExamination`) decide the course an application runs, and are
|
||||
* read live rather than snapshotted. The server answers 409
|
||||
* `license_type_in_use` when changing one would strand applications that
|
||||
* have not yet had their approval decision.
|
||||
*/
|
||||
updateLicenseBehavior: builder.mutation<
|
||||
LicenseType,
|
||||
{
|
||||
id: string;
|
||||
workflowProfile?: WorkflowProfile;
|
||||
serviceKind?: ServiceKind;
|
||||
completionEffect?: CompletionEffect | null;
|
||||
certificateCategory?: CertificateCategory | null;
|
||||
requiresExamination?: boolean;
|
||||
inspectionRequired?: boolean;
|
||||
issuesCertificate?: boolean;
|
||||
renewalEnabled?: boolean;
|
||||
requiresSeafarerRegistration?: boolean;
|
||||
requiresValidMedical?: boolean;
|
||||
minSeaTimeDays?: number | null;
|
||||
validityMonths?: number;
|
||||
validityDays?: number | null;
|
||||
capitalThreshold?: number | null;
|
||||
renewalWindowDays?: number;
|
||||
expiryReminderDays?: number[];
|
||||
requiresOperatorMode?: boolean;
|
||||
allowMultipleOpenDrafts?: boolean;
|
||||
requiresIssuanceScheduling?: boolean;
|
||||
uniqueFormKeyPath?: string | null;
|
||||
slaHours?: number | null;
|
||||
}
|
||||
>({
|
||||
query: ({ id, ...body }) => ({
|
||||
url: `/license-types/${id}/behavior`,
|
||||
method: 'PATCH',
|
||||
body,
|
||||
}),
|
||||
invalidatesTags: (_r, error, { id }) =>
|
||||
error ? [] : [listTag('LicenseType'), itemTag('LicenseType', id)],
|
||||
}),
|
||||
|
||||
/** Validity is edited beside the certificate design, not with the fees. */
|
||||
updateLicenseValidity: builder.mutation<
|
||||
LicenseType,
|
||||
@@ -234,9 +320,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')]),
|
||||
@@ -337,6 +444,11 @@ export const licensingApi = baseApi
|
||||
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseApplication')]),
|
||||
}),
|
||||
|
||||
discardApplication: builder.mutation<{ deleted: boolean }, string>({
|
||||
query: (id) => ({ url: `/license-applications/${id}`, method: 'DELETE' }),
|
||||
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseApplication')]),
|
||||
}),
|
||||
|
||||
getMyApplications: builder.query<Paginated<LicenseApplication>, void>({
|
||||
query: () => ({ url: '/license-applications/mine' }),
|
||||
providesTags: () => [listTag('LicenseApplication')],
|
||||
@@ -958,12 +1070,12 @@ export const licensingApi = baseApi
|
||||
|
||||
scheduleIssuance: builder.mutation<
|
||||
LicenseApplication,
|
||||
{ id: string; scheduledDate: string }
|
||||
{ id: string; scheduledDate: string; scheduledPeriod: IssuancePeriod }
|
||||
>({
|
||||
query: ({ id, scheduledDate }) => ({
|
||||
query: ({ id, scheduledDate, scheduledPeriod }) => ({
|
||||
url: `/license-application-review/${id}/schedule-issuance`,
|
||||
method: 'POST',
|
||||
body: { scheduledDate },
|
||||
body: { scheduledDate, scheduledPeriod },
|
||||
}),
|
||||
invalidatesTags: (_r, error, { id }) =>
|
||||
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
||||
@@ -978,6 +1090,104 @@ export const licensingApi = baseApi
|
||||
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
||||
}),
|
||||
|
||||
// ------------------------------------------------------------- pickup
|
||||
getPickupOffices: builder.query<PickupOffice[], void>({
|
||||
query: () => ({ url: '/pickup/offices' }),
|
||||
providesTags: [listTag('PickupOffice')],
|
||||
}),
|
||||
|
||||
getPickupSlots: builder.query<
|
||||
PickupSlot[],
|
||||
{ officeId: string; from: string; to: string }
|
||||
>({
|
||||
query: ({ officeId, from, to }) => ({
|
||||
url: `/pickup/offices/${officeId}/slots`,
|
||||
params: { from, to },
|
||||
}),
|
||||
}),
|
||||
|
||||
schedulePickup: builder.mutation<
|
||||
PickupAppointment,
|
||||
{ applicationId: string; officeId: string; date: string; slotStartTime: string }
|
||||
>({
|
||||
query: (body) => ({ url: '/pickup/appointments', method: 'POST', body }),
|
||||
invalidatesTags: (_r, error, { applicationId }) =>
|
||||
error
|
||||
? []
|
||||
: [
|
||||
itemTag('LicenseApplication', applicationId),
|
||||
listTag('ApplicationQueue'),
|
||||
listTag('PickupAppointment'),
|
||||
],
|
||||
}),
|
||||
|
||||
reschedulePickup: builder.mutation<
|
||||
PickupAppointment,
|
||||
{ appointmentId: string; officeId: string; date: string; slotStartTime: string }
|
||||
>({
|
||||
query: ({ appointmentId, ...body }) => ({
|
||||
url: `/pickup/appointments/${appointmentId}/reschedule`,
|
||||
method: 'PATCH',
|
||||
body,
|
||||
}),
|
||||
invalidatesTags: (_r, error, { appointmentId }) =>
|
||||
error ? [] : [itemTag('PickupAppointment', appointmentId), listTag('PickupAppointment')],
|
||||
}),
|
||||
|
||||
getPickupAppointmentsForApplication: builder.query<PickupAppointment[], string>({
|
||||
query: (applicationId) => ({
|
||||
url: `/pickup/applications/${applicationId}/appointments`,
|
||||
}),
|
||||
providesTags: (_r, _e, applicationId) => [itemTag('PickupAppointment', applicationId)],
|
||||
}),
|
||||
|
||||
getPickupWorklist: builder.query<
|
||||
PickupAppointment[],
|
||||
{ date: string; officeId?: string }
|
||||
>({
|
||||
query: ({ date, officeId }) => ({
|
||||
url: '/pickup/appointments',
|
||||
params: officeId ? { date, officeId } : { date },
|
||||
}),
|
||||
providesTags: [listTag('PickupAppointment')],
|
||||
}),
|
||||
|
||||
checkInPickup: builder.mutation<PickupAppointment, string>({
|
||||
query: (id) => ({ url: `/pickup/appointments/${id}/check-in`, method: 'PATCH' }),
|
||||
invalidatesTags: (_r, error, id) =>
|
||||
error ? [] : [itemTag('PickupAppointment', id), listTag('PickupAppointment')],
|
||||
}),
|
||||
|
||||
markPickupIssued: builder.mutation<PickupAppointment, string>({
|
||||
query: (id) => ({ url: `/pickup/appointments/${id}/issued`, method: 'PATCH' }),
|
||||
invalidatesTags: (_r, error, id) =>
|
||||
error ? [] : [itemTag('PickupAppointment', id), listTag('PickupAppointment')],
|
||||
}),
|
||||
|
||||
markPickupNoShow: builder.mutation<PickupAppointment, string>({
|
||||
query: (id) => ({ url: `/pickup/appointments/${id}/no-show`, method: 'PATCH' }),
|
||||
invalidatesTags: (_r, error, id) =>
|
||||
error ? [] : [itemTag('PickupAppointment', id), listTag('PickupAppointment')],
|
||||
}),
|
||||
|
||||
createPickupOffice: builder.mutation<PickupOffice, Partial<PickupOffice>>({
|
||||
query: (body) => ({ url: '/pickup/offices', method: 'POST', body }),
|
||||
invalidatesTags: [listTag('PickupOffice')],
|
||||
}),
|
||||
|
||||
updatePickupOffice: builder.mutation<
|
||||
PickupOffice,
|
||||
{ id: string } & Partial<PickupOffice>
|
||||
>({
|
||||
query: ({ id, ...body }) => ({
|
||||
url: `/pickup/offices/${id}`,
|
||||
method: 'PATCH',
|
||||
body,
|
||||
}),
|
||||
invalidatesTags: (_r, error, { id }) =>
|
||||
error ? [] : [itemTag('PickupOffice', id), listTag('PickupOffice')],
|
||||
}),
|
||||
|
||||
// --------------------------------------------------------- inspection
|
||||
scheduleInspection: builder.mutation<
|
||||
Inspection,
|
||||
@@ -993,6 +1203,30 @@ export const licensingApi = baseApi
|
||||
error ? [] : [itemTag('LicenseApplication', applicationId), listTag('Inspection')],
|
||||
}),
|
||||
|
||||
/** Moves a booked visit: another day, slot, inspector or place. */
|
||||
rescheduleInspection: builder.mutation<
|
||||
Inspection,
|
||||
{
|
||||
inspectionId: string;
|
||||
applicationId: string;
|
||||
scheduledDate: string;
|
||||
timeSlot: 'MORNING' | 'AFTERNOON';
|
||||
inspectorId?: string;
|
||||
location?: string;
|
||||
reason?: string;
|
||||
}
|
||||
>({
|
||||
query: ({ inspectionId, scheduledDate, timeSlot, inspectorId, location, reason }) => ({
|
||||
url: `/inspections/${inspectionId}/schedule`,
|
||||
method: 'PATCH',
|
||||
// applicationId is for cache invalidation only; the visit knows its
|
||||
// own application.
|
||||
body: { scheduledDate, timeSlot, inspectorId, location, reason },
|
||||
}),
|
||||
invalidatesTags: (_r, error, { applicationId }) =>
|
||||
error ? [] : [itemTag('LicenseApplication', applicationId), listTag('Inspection')],
|
||||
}),
|
||||
|
||||
getInspections: builder.query<Inspection[], string>({
|
||||
query: (applicationId) => ({ url: `/inspections/application/${applicationId}` }),
|
||||
providesTags: () => [listTag('Inspection')],
|
||||
@@ -1046,10 +1280,12 @@ export const {
|
||||
useGetLicenseTypesQuery,
|
||||
useGetLicenseCategoriesQuery,
|
||||
useUpdateLicenseFeesMutation,
|
||||
useUpdateLicenseBehaviorMutation,
|
||||
useUpdateFormSchemaMutation,
|
||||
useValidateFormSchemaMutation,
|
||||
useGetFormSchemaPaletteQuery,
|
||||
useGetDocumentRequirementsQuery,
|
||||
useGetPersonalDocumentsQuery,
|
||||
useCreateDocumentRequirementMutation,
|
||||
useUpdateDocumentRequirementMutation,
|
||||
useDeleteDocumentRequirementMutation,
|
||||
@@ -1067,6 +1303,7 @@ export const {
|
||||
useGetLicenseTypeRequirementsQuery,
|
||||
useCreateApplicationMutation,
|
||||
useGetExamStateForApplicationQuery,
|
||||
useDiscardApplicationMutation,
|
||||
useGetMyApplicationsQuery,
|
||||
useGetApplicationQuery,
|
||||
useInitiatePaymentMutation,
|
||||
@@ -1127,7 +1364,19 @@ export const {
|
||||
useConfirmPaymentMutation,
|
||||
useScheduleIssuanceMutation,
|
||||
useIssueCertificateMutation,
|
||||
useGetPickupOfficesQuery,
|
||||
useGetPickupSlotsQuery,
|
||||
useSchedulePickupMutation,
|
||||
useReschedulePickupMutation,
|
||||
useGetPickupAppointmentsForApplicationQuery,
|
||||
useGetPickupWorklistQuery,
|
||||
useCheckInPickupMutation,
|
||||
useMarkPickupIssuedMutation,
|
||||
useMarkPickupNoShowMutation,
|
||||
useCreatePickupOfficeMutation,
|
||||
useUpdatePickupOfficeMutation,
|
||||
useScheduleInspectionMutation,
|
||||
useRescheduleInspectionMutation,
|
||||
useGetInspectionsQuery,
|
||||
useRecordInspectionResultMutation,
|
||||
useGetNotificationsQuery,
|
||||
|
||||
Reference in New Issue
Block a user