mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
Seafarer and vessel registration were filtered out of the operations step by `requiresOperatorMode !== false`, so someone registering as a seafarer or vessel owner landed on an onboarding screen that did not describe them. Both now appear, grouped apart from the company modes: declaring "I am a seafarer" is a different kind of statement from "my company forwards freight". The designer's preview was gated on the Handlebars source alone, which a canvas layout does not have until the server compiles it on save -- so the button was dead for exactly the designs the canvas exists for. Editing looked broken rather than deliberately read-only: every seeded template is PUBLISHED, and a published design is immutable because certificates were issued from it. Says so, and offers the new-version action that is the way forward. Reviewing officers saw company, capital and staff tabs on seafarer certificate applications, because PRESENTATION is keyed by the generic licence keys and the fifty-odd rank-specific CoC/CoP keys fell through to the company default. Matched by prefix instead, so a certificate configured tomorrow gets the right presentation without a code change. Seaman book and BTC are wired to the newly seeded licence types and no longer marked "soon". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
853 lines
30 KiB
TypeScript
853 lines
30 KiB
TypeScript
import { baseApi } from '../../base-api';
|
|
import type {
|
|
AppNotification,
|
|
ApplicationDetail,
|
|
ApplicationKind,
|
|
ApplicationPayment,
|
|
ApplicationStaff,
|
|
Attachment,
|
|
InitiatePaymentResult,
|
|
IssuedLicense,
|
|
Inspection,
|
|
LicenseApplication,
|
|
LicenseCategoryDefinition,
|
|
LicenseStatus,
|
|
LicenseType,
|
|
LicenseTypeRequirements,
|
|
OperatorType,
|
|
AssignableOfficer,
|
|
DocumentDecision,
|
|
DocumentReview,
|
|
ExportResult,
|
|
LicenseTemplate,
|
|
Paginated,
|
|
QueueCounts,
|
|
QueueFilter,
|
|
RemarkTargetType,
|
|
SavedQueueView,
|
|
TemplateFieldPlacement,
|
|
TemplateLogoPlacement,
|
|
TemplatePageOptions,
|
|
TemplateVariable,
|
|
} from './licensing.types';
|
|
|
|
/**
|
|
* Drops empty filters and flattens the status array.
|
|
*
|
|
* The grid keeps every facet in one object and serialises it to the URL, so
|
|
* cleared facets arrive as undefined/[]; sending those verbatim would put
|
|
* `status=` and `assignee=` on the wire and fail DTO validation.
|
|
*/
|
|
function serialiseQueueFilter(
|
|
filter: QueueFilter | void,
|
|
): Record<string, unknown> {
|
|
if (!filter) return {};
|
|
const params: Record<string, unknown> = {};
|
|
for (const [key, value] of Object.entries(filter)) {
|
|
if (value === undefined || value === null || value === '') continue;
|
|
if (Array.isArray(value)) {
|
|
if (value.length === 0) continue;
|
|
params[key] = value.join(',');
|
|
continue;
|
|
}
|
|
params[key] = value;
|
|
}
|
|
return params;
|
|
}
|
|
|
|
const TAGS = [
|
|
'LicenseType',
|
|
'OperatorType',
|
|
'LicenseApplication',
|
|
'ApplicationQueue',
|
|
'Attachment',
|
|
'Notification',
|
|
'Inspection',
|
|
'License',
|
|
'SavedView',
|
|
'LicenseTemplate',
|
|
] as const;
|
|
|
|
const listTag = (type: (typeof TAGS)[number]) => ({ type, id: 'LIST' }) as const;
|
|
const itemTag = (type: (typeof TAGS)[number], id: string) => ({ type, id }) as const;
|
|
|
|
/**
|
|
* Typed licensing endpoints, shared by the portal and the backoffice.
|
|
*
|
|
* Each resource gets its own cache tag so a transition invalidates the one
|
|
* application and its queue, rather than the whole store.
|
|
*/
|
|
export const licensingApi = baseApi
|
|
.enhanceEndpoints({ addTagTypes: TAGS })
|
|
.injectEndpoints({
|
|
endpoints: (builder) => ({
|
|
// ------------------------------------------------------------- config
|
|
/** Active licence types an applicant can choose from. */
|
|
getLicenseTypes: builder.query<Paginated<LicenseType>, void>({
|
|
query: () => ({ url: '/license-types' }),
|
|
providesTags: () => [listTag('LicenseType')],
|
|
}),
|
|
|
|
/**
|
|
* The signed-in applicant's declared modes of operation.
|
|
*
|
|
* Separate from the licence-type catalogue on purpose: the catalogue is
|
|
* the same for everyone and heavily cached, while this is per-user and
|
|
* changes the moment they edit their profile.
|
|
*/
|
|
getMyOperatorTypes: builder.query<{ items: OperatorType[] }, void>({
|
|
query: () => ({ url: '/profiles/me/operations' }),
|
|
providesTags: () => [listTag('OperatorType')],
|
|
}),
|
|
|
|
/** Replaces the set — see the Operations tab in the portal profile. */
|
|
updateMyOperatorTypes: builder.mutation<
|
|
{ items: OperatorType[] },
|
|
{ licenseTypeIds: string[] }
|
|
>({
|
|
query: (body) => ({
|
|
url: '/profiles/me/operations',
|
|
method: 'PUT',
|
|
body,
|
|
}),
|
|
// The catalogue is filtered by this, so it has to refetch too.
|
|
invalidatesTags: (_r, error) =>
|
|
error ? [] : [listTag('OperatorType'), listTag('LicenseType')],
|
|
}),
|
|
|
|
/**
|
|
* Category catalogue used to group the licence cards. Static on the
|
|
* server, so it is cached under the licence-type list tag.
|
|
*/
|
|
getLicenseCategories: builder.query<
|
|
Paginated<LicenseCategoryDefinition>,
|
|
void
|
|
>({
|
|
query: () => ({ url: '/license-categories' }),
|
|
providesTags: () => [listTag('LicenseType')],
|
|
}),
|
|
|
|
/**
|
|
* Fee configuration, for licensing administrators.
|
|
*
|
|
* `null` is a meaningful value for both amounts and must survive the
|
|
* round trip: null on the new-application fee means the licence is free,
|
|
* null on the renewal fee means it is charged at the new-application
|
|
* rate. Sending `undefined` instead would leave the column untouched.
|
|
*/
|
|
updateLicenseFees: builder.mutation<
|
|
LicenseType,
|
|
{
|
|
id: string;
|
|
feeNewApplication?: number | null;
|
|
feeRenewal?: number | null;
|
|
feeCurrency?: string;
|
|
}
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-types/${id}/fees`,
|
|
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,
|
|
{ id: string; validityMonths: number }
|
|
>({
|
|
query: ({ id, validityMonths }) => ({
|
|
url: `/license-types/${id}/validity`,
|
|
method: 'PATCH',
|
|
body: { validityMonths },
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseType', id), listTag('LicenseType')],
|
|
}),
|
|
|
|
getLicenseTypeRequirements: builder.query<
|
|
LicenseTypeRequirements,
|
|
{ idOrKey: string; kind?: ApplicationKind }
|
|
>({
|
|
query: ({ idOrKey, kind = 'NEW' }) => ({
|
|
url: `/license-types/requirements/${idOrKey}`,
|
|
params: { kind },
|
|
}),
|
|
providesTags: (_r, _e, arg) => [itemTag('LicenseType', arg.idOrKey)],
|
|
}),
|
|
|
|
// -------------------------------------------------------- application
|
|
createApplication: builder.mutation<
|
|
LicenseApplication,
|
|
{ licenseType: string; kind?: ApplicationKind; previousLicenseId?: string }
|
|
>({
|
|
query: (body) => ({ url: '/license-applications', method: 'POST', body }),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseApplication')]),
|
|
}),
|
|
|
|
getMyApplications: builder.query<Paginated<LicenseApplication>, void>({
|
|
query: () => ({ url: '/license-applications/mine' }),
|
|
providesTags: () => [listTag('LicenseApplication')],
|
|
}),
|
|
|
|
getApplication: builder.query<ApplicationDetail, string>({
|
|
query: (id) => ({ url: `/license-applications/${id}` }),
|
|
providesTags: (_r, _e, id) => [itemTag('LicenseApplication', id)],
|
|
}),
|
|
|
|
patchSection: builder.mutation<
|
|
LicenseApplication,
|
|
{ id: string; sectionKey: string; values: Record<string, unknown> }
|
|
>({
|
|
query: ({ id, sectionKey, values }) => ({
|
|
url: `/license-applications/${id}/sections/${sectionKey}`,
|
|
method: 'PATCH',
|
|
body: { values },
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id)],
|
|
}),
|
|
|
|
addStaff: builder.mutation<
|
|
ApplicationStaff,
|
|
{ id: string; roleKey: string; fullName: string; position?: string; yearsOfExperience?: number }
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-applications/${id}/staff`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id)],
|
|
}),
|
|
|
|
removeStaff: builder.mutation<unknown, { id: string; staffId: string }>({
|
|
query: ({ id, staffId }) => ({
|
|
url: `/license-applications/${id}/staff/${staffId}`,
|
|
method: 'DELETE',
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id)],
|
|
}),
|
|
|
|
submitApplication: builder.mutation<LicenseApplication, string>({
|
|
query: (id) => ({ url: `/license-applications/${id}/submit`, method: 'POST' }),
|
|
invalidatesTags: (_r, error, id) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('LicenseApplication'), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
resolveRemark: builder.mutation<unknown, { id: string; remarkId: string }>({
|
|
query: ({ id, remarkId }) => ({
|
|
url: `/license-applications/${id}/remarks/${remarkId}/resolve`,
|
|
method: 'PATCH',
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id)],
|
|
}),
|
|
|
|
resubmitApplication: builder.mutation<LicenseApplication, string>({
|
|
query: (id) => ({ url: `/license-applications/${id}/resubmit`, method: 'POST' }),
|
|
invalidatesTags: (_r, error, id) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('LicenseApplication')],
|
|
}),
|
|
|
|
// -------------------------------------------------------- attachments
|
|
getAttachments: builder.query<Attachment[], { ownerType: string; ownerId: string }>({
|
|
query: ({ ownerType, ownerId }) => ({
|
|
url: '/attachments',
|
|
params: { ownerType, ownerId, withUrls: true },
|
|
}),
|
|
providesTags: (_r, _e, arg) => [itemTag('Attachment', arg.ownerId)],
|
|
}),
|
|
|
|
deleteAttachment: builder.mutation<unknown, { attachmentId: string; ownerId: string }>({
|
|
query: ({ attachmentId }) => ({
|
|
url: `/attachments/${attachmentId}`,
|
|
method: 'DELETE',
|
|
}),
|
|
invalidatesTags: (_r, error, { ownerId }) =>
|
|
error ? [] : [itemTag('Attachment', ownerId)],
|
|
}),
|
|
|
|
// ----------------------------------------------------------- payments
|
|
initiatePayment: builder.mutation<
|
|
InitiatePaymentResult,
|
|
{ id: string; provider?: string; platform?: 'web' | 'mobile'; payerAccount?: string }
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-applications/${id}/payments/initiate`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id)],
|
|
}),
|
|
|
|
getApplicationPayment: builder.query<ApplicationPayment, string>({
|
|
query: (id) => ({ url: `/license-applications/${id}/payments` }),
|
|
providesTags: (_r, _e, id) => [itemTag('LicenseApplication', id)],
|
|
}),
|
|
|
|
/** Testing shortcut — only offered when the API reports it enabled. */
|
|
bypassPayment: builder.mutation<
|
|
{ status: LicenseStatus; certificateIssued: boolean },
|
|
string
|
|
>({
|
|
query: (id) => ({
|
|
url: `/license-applications/${id}/payments/bypass`,
|
|
method: 'POST',
|
|
}),
|
|
invalidatesTags: (_r, error, id) =>
|
|
error
|
|
? []
|
|
: [itemTag('LicenseApplication', id), listTag('LicenseApplication'), listTag('License')],
|
|
}),
|
|
|
|
getPaymentCapabilities: builder.query<{ bypassEnabled: boolean }, void>({
|
|
query: () => ({ url: '/license-applications/payments/capabilities' }),
|
|
}),
|
|
|
|
// ------------------------------------------------------------ licences
|
|
/** The licence register for enforcement officers. */
|
|
getLicenses: builder.query<
|
|
Paginated<IssuedLicense>,
|
|
{ search?: string } | void
|
|
>({
|
|
query: (args) => ({
|
|
url: '/licenses',
|
|
params: args?.search ? { search: args.search } : undefined,
|
|
}),
|
|
providesTags: () => [listTag('License')],
|
|
}),
|
|
|
|
getMyLicenses: builder.query<Paginated<IssuedLicense>, void>({
|
|
query: () => ({ url: '/licenses/mine' }),
|
|
providesTags: () => [listTag('License')],
|
|
}),
|
|
|
|
suspendLicense: builder.mutation<IssuedLicense, { id: string; reason: string }>({
|
|
query: ({ id, reason }) => ({
|
|
url: `/licenses/${id}/suspend`,
|
|
method: 'POST',
|
|
body: { reason },
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('License', id), listTag('License')],
|
|
}),
|
|
|
|
revokeLicense: builder.mutation<IssuedLicense, { id: string; reason: string }>({
|
|
query: ({ id, reason }) => ({
|
|
url: `/licenses/${id}/revoke`,
|
|
method: 'POST',
|
|
body: { reason },
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('License', id), listTag('License')],
|
|
}),
|
|
|
|
reinstateLicense: builder.mutation<IssuedLicense, { id: string; reason: string }>({
|
|
query: ({ id, reason }) => ({
|
|
url: `/licenses/${id}/reinstate`,
|
|
method: 'POST',
|
|
body: { reason },
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('License', id), listTag('License')],
|
|
}),
|
|
|
|
getCertificateUrl: builder.mutation<{ url: string }, string>({
|
|
query: (id) => ({ url: `/licenses/${id}/certificate` }),
|
|
}),
|
|
|
|
// ------------------------------------------------------------- review
|
|
getQueue: builder.query<Paginated<LicenseApplication>, QueueFilter | void>({
|
|
query: (params) => ({
|
|
url: '/license-application-review/queue',
|
|
params: serialiseQueueFilter(params),
|
|
}),
|
|
providesTags: () => [listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
getAssignedToMe: builder.query<
|
|
Paginated<LicenseApplication>,
|
|
QueueFilter | void
|
|
>({
|
|
query: (params) => ({
|
|
url: '/license-application-review/assigned-to-me',
|
|
params: serialiseQueueFilter(params),
|
|
}),
|
|
providesTags: () => [listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
/** Every non-draft application — backs the grid's "All" view. */
|
|
getAllApplications: builder.query<
|
|
Paginated<LicenseApplication>,
|
|
QueueFilter | void
|
|
>({
|
|
query: (params) => ({
|
|
url: '/license-application-review/all',
|
|
params: serialiseQueueFilter(params),
|
|
}),
|
|
providesTags: () => [listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
/**
|
|
* Counts for the saved-view tabs. Shares the ApplicationQueue tag, so a
|
|
* claim or a decision refreshes the badges along with the list and the
|
|
* numbers can never drift from what the grid is showing.
|
|
*/
|
|
getQueueCounts: builder.query<QueueCounts, void>({
|
|
query: () => ({ url: '/license-application-review/counts' }),
|
|
providesTags: () => [listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
getApplicationForReview: builder.query<ApplicationDetail, string>({
|
|
query: (id) => ({ url: `/license-application-review/${id}` }),
|
|
providesTags: (_r, _e, id) => [itemTag('LicenseApplication', id)],
|
|
}),
|
|
|
|
claimApplication: builder.mutation<LicenseApplication, string>({
|
|
query: (id) => ({ url: `/license-application-review/${id}/claim`, method: 'POST' }),
|
|
invalidatesTags: (_r, error, id) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
completeReview: builder.mutation<
|
|
LicenseApplication,
|
|
{ id: string; capitalAmountVerified?: number; remark?: string }
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-application-review/${id}/complete-review`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
requestAdjustment: builder.mutation<
|
|
LicenseApplication,
|
|
{
|
|
id: string;
|
|
generalRemark?: string;
|
|
items: { targetType: RemarkTargetType; targetKey: string; remark: string }[];
|
|
/** Officer-edited wording for the applicant notification. */
|
|
notificationBody?: string;
|
|
}
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-application-review/${id}/request-adjustment`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
approveDocuments: builder.mutation<LicenseApplication, { id: string; remark?: string }>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-application-review/${id}/approve-documents`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
/**
|
|
* `capitalAmountVerified` is what the officer confirms against the bank
|
|
* letter. Licence types with a capital threshold reject the approval
|
|
* without it, so it is part of the contract, not an optional extra.
|
|
*/
|
|
finalApprove: builder.mutation<
|
|
LicenseApplication,
|
|
{ id: string; remark?: string; capitalAmountVerified?: number }
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-application-review/${id}/final-approve`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
rejectApplication: builder.mutation<
|
|
LicenseApplication,
|
|
{ id: string; reason: string; notificationBody?: string }
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-application-review/${id}/reject`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
/** Places a candidate who has paid the examination fee into a sitting. */
|
|
scheduleExam: builder.mutation<
|
|
LicenseApplication,
|
|
{ id: string; examId: string; admissionNumber?: string; examDate?: string }
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-application-review/${id}/schedule-exam`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
/**
|
|
* Raises the examination fee — after eligibility approval, or again when
|
|
* a failed candidate elects to resit.
|
|
*/
|
|
requestExamPayment: builder.mutation<LicenseApplication, string>({
|
|
query: (id) => ({
|
|
url: `/license-applications/${id}/request-exam-payment`,
|
|
method: 'POST',
|
|
}),
|
|
invalidatesTags: (_r, error, id) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('LicenseApplication')],
|
|
}),
|
|
|
|
// ------------------------------------------------- certificate designs
|
|
getLicenseTemplates: builder.query<LicenseTemplate[], string | void>({
|
|
query: (licenseTypeId) => ({
|
|
url: '/license-templates',
|
|
params: licenseTypeId ? { licenseTypeId } : {},
|
|
}),
|
|
providesTags: () => [listTag('LicenseTemplate')],
|
|
}),
|
|
|
|
getTemplateVariables: builder.query<TemplateVariable[], void>({
|
|
query: () => ({ url: '/license-templates/variables' }),
|
|
}),
|
|
|
|
getBuiltInTemplate: builder.query<{ hbsSource: string }, void>({
|
|
query: () => ({ url: '/license-templates/built-in' }),
|
|
}),
|
|
|
|
createLicenseTemplate: builder.mutation<
|
|
LicenseTemplate,
|
|
{
|
|
licenseTypeId: string;
|
|
name: string;
|
|
hbsSource?: string;
|
|
pageOptions?: TemplatePageOptions;
|
|
}
|
|
>({
|
|
query: (body) => ({ url: '/license-templates', method: 'POST', body }),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseTemplate')]),
|
|
}),
|
|
|
|
updateLicenseTemplate: builder.mutation<
|
|
LicenseTemplate,
|
|
{
|
|
id: string;
|
|
name?: string;
|
|
hbsSource?: string;
|
|
pageOptions?: TemplatePageOptions;
|
|
backgroundUrl?: string;
|
|
logoUrl?: string;
|
|
logoPlacement?: TemplateLogoPlacement;
|
|
fieldPlacements?: TemplateFieldPlacement[];
|
|
}
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-templates/${id}`,
|
|
method: 'PATCH',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseTemplate')]),
|
|
}),
|
|
|
|
publishLicenseTemplate: builder.mutation<LicenseTemplate, string>({
|
|
query: (id) => ({ url: `/license-templates/${id}/publish`, method: 'POST' }),
|
|
// The previously published design is archived by the same call, so the
|
|
// whole list is refetched rather than patching two rows by hand.
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseTemplate')]),
|
|
}),
|
|
|
|
archiveLicenseTemplate: builder.mutation<LicenseTemplate, string>({
|
|
query: (id) => ({ url: `/license-templates/${id}/archive`, method: 'POST' }),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseTemplate')]),
|
|
}),
|
|
|
|
deleteLicenseTemplate: builder.mutation<unknown, string>({
|
|
query: (id) => ({ url: `/license-templates/${id}`, method: 'DELETE' }),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseTemplate')]),
|
|
}),
|
|
|
|
// ------------------------------------------------------ document review
|
|
getDocumentReviews: builder.query<DocumentReview[], string>({
|
|
query: (id) => ({ url: `/license-application-review/${id}/document-reviews` }),
|
|
providesTags: (_r, _e, id) => [itemTag('LicenseApplication', id)],
|
|
}),
|
|
|
|
reviewDocument: builder.mutation<
|
|
DocumentReview,
|
|
{
|
|
id: string;
|
|
documentKey: string;
|
|
decision: DocumentDecision;
|
|
reason?: string;
|
|
attachmentId?: string;
|
|
}
|
|
>({
|
|
query: ({ id, documentKey, ...body }) => ({
|
|
url: `/license-application-review/${id}/documents/${encodeURIComponent(documentKey)}/review`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id)],
|
|
}),
|
|
|
|
clearDocumentReview: builder.mutation<unknown, { id: string; documentKey: string }>({
|
|
query: ({ id, documentKey }) => ({
|
|
url: `/license-application-review/${id}/documents/${encodeURIComponent(documentKey)}/review`,
|
|
method: 'DELETE',
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id)],
|
|
}),
|
|
|
|
// ----------------------------------------------------------- saved views
|
|
getSavedViews: builder.query<SavedQueueView[], void>({
|
|
query: () => ({ url: '/license-application-review/saved-views' }),
|
|
providesTags: () => [listTag('SavedView')],
|
|
}),
|
|
|
|
createSavedView: builder.mutation<
|
|
SavedQueueView,
|
|
{ name: string; queryString: string; isShared?: boolean; isDefault?: boolean }
|
|
>({
|
|
query: (body) => ({
|
|
url: '/license-application-review/saved-views',
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('SavedView')]),
|
|
}),
|
|
|
|
deleteSavedView: builder.mutation<unknown, string>({
|
|
query: (viewId) => ({
|
|
url: `/license-application-review/saved-views/${viewId}`,
|
|
method: 'DELETE',
|
|
}),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('SavedView')]),
|
|
}),
|
|
|
|
// ------------------------------------------------------------- export
|
|
/**
|
|
* The whole filtered result set, not the current page. Lazy so it only
|
|
* runs when the officer actually asks for a file.
|
|
*/
|
|
exportApplications: builder.query<ExportResult, QueueFilter | void>({
|
|
query: (params) => ({
|
|
url: '/license-application-review/export',
|
|
params: serialiseQueueFilter(params),
|
|
}),
|
|
}),
|
|
|
|
/** Officers the Assign and Escalate dialogs can offer. */
|
|
getAssignableOfficers: builder.query<AssignableOfficer[], void>({
|
|
query: () => ({ url: '/license-application-review/officers' }),
|
|
}),
|
|
|
|
// ----------------------------------------------------- workflow controls
|
|
assignApplication: builder.mutation<
|
|
LicenseApplication,
|
|
{ id: string; officerId: string; remark?: string }
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-application-review/${id}/assign`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
holdApplication: builder.mutation<
|
|
LicenseApplication,
|
|
{ id: string; reason: string }
|
|
>({
|
|
query: ({ id, reason }) => ({
|
|
url: `/license-application-review/${id}/hold`,
|
|
method: 'POST',
|
|
body: { reason },
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
resumeApplication: builder.mutation<
|
|
LicenseApplication,
|
|
{ id: string; remark?: string }
|
|
>({
|
|
query: ({ id, remark }) => ({
|
|
url: `/license-application-review/${id}/resume`,
|
|
method: 'POST',
|
|
body: { remark },
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
escalateApplication: builder.mutation<
|
|
LicenseApplication,
|
|
{ id: string; supervisorId: string; reason: string }
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-application-review/${id}/escalate`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
confirmPayment: builder.mutation<LicenseApplication, string>({
|
|
query: (id) => ({
|
|
url: `/license-application-review/${id}/confirm-payment`,
|
|
method: 'POST',
|
|
}),
|
|
invalidatesTags: (_r, error, id) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
// --------------------------------------------------------- inspection
|
|
scheduleInspection: builder.mutation<
|
|
Inspection,
|
|
{ applicationId: string; scheduledDate: string; location?: string }
|
|
>({
|
|
query: (body) => ({ url: '/inspections', method: 'POST', body }),
|
|
invalidatesTags: (_r, error, { applicationId }) =>
|
|
error ? [] : [itemTag('LicenseApplication', applicationId), listTag('Inspection')],
|
|
}),
|
|
|
|
getInspections: builder.query<Inspection[], string>({
|
|
query: (applicationId) => ({ url: `/inspections/application/${applicationId}` }),
|
|
providesTags: () => [listTag('Inspection')],
|
|
}),
|
|
|
|
recordInspectionResult: builder.mutation<
|
|
Inspection,
|
|
{
|
|
inspectionId: string;
|
|
applicationId: string;
|
|
result: 'PASSED' | 'FAILED';
|
|
findings: string;
|
|
/** Structured per-item outcomes (office, warehouse, vehicles, …). */
|
|
checklist?: {
|
|
key: string;
|
|
label: string;
|
|
outcome: 'PASS' | 'FAIL' | 'NEEDS_CORRECTION';
|
|
note?: string;
|
|
}[];
|
|
}
|
|
>({
|
|
query: ({ inspectionId, result, findings, checklist }) => ({
|
|
url: `/inspections/${inspectionId}/result`,
|
|
method: 'PATCH',
|
|
body: { result, findings, ...(checklist?.length ? { checklist } : {}) },
|
|
}),
|
|
invalidatesTags: (_r, error, { applicationId }) =>
|
|
error ? [] : [itemTag('LicenseApplication', applicationId), listTag('Inspection')],
|
|
}),
|
|
|
|
// ------------------------------------------------------ notifications
|
|
getNotifications: builder.query<{ count: number; items: AppNotification[] }, void>({
|
|
query: () => ({ url: '/notifications' }),
|
|
providesTags: () => [listTag('Notification')],
|
|
}),
|
|
|
|
getUnseenNotifications: builder.query<{ count: number; items: AppNotification[] }, void>({
|
|
query: () => ({ url: '/notifications/unseen' }),
|
|
providesTags: () => [listTag('Notification')],
|
|
}),
|
|
|
|
markNotificationRead: builder.mutation<unknown, string>({
|
|
query: (id) => ({ url: `/notifications/${id}/read`, method: 'PATCH' }),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('Notification')]),
|
|
}),
|
|
}),
|
|
overrideExisting: false,
|
|
});
|
|
|
|
export const {
|
|
useGetLicenseTypesQuery,
|
|
useGetLicenseCategoriesQuery,
|
|
useUpdateLicenseFeesMutation,
|
|
useUpdateLicenseValidityMutation,
|
|
useGetLicenseTypeRequirementsQuery,
|
|
useCreateApplicationMutation,
|
|
useGetMyApplicationsQuery,
|
|
useGetApplicationQuery,
|
|
useInitiatePaymentMutation,
|
|
useBypassPaymentMutation,
|
|
useGetPaymentCapabilitiesQuery,
|
|
useGetMyLicensesQuery,
|
|
useGetLicensesQuery,
|
|
useGetCertificateUrlMutation,
|
|
useGetApplicationPaymentQuery,
|
|
usePatchSectionMutation,
|
|
useAddStaffMutation,
|
|
useRemoveStaffMutation,
|
|
useSubmitApplicationMutation,
|
|
useResolveRemarkMutation,
|
|
useResubmitApplicationMutation,
|
|
useGetAttachmentsQuery,
|
|
useDeleteAttachmentMutation,
|
|
useGetQueueQuery,
|
|
useGetAssignedToMeQuery,
|
|
useGetAllApplicationsQuery,
|
|
useGetQueueCountsQuery,
|
|
useGetLicenseTemplatesQuery,
|
|
useGetTemplateVariablesQuery,
|
|
useGetBuiltInTemplateQuery,
|
|
useCreateLicenseTemplateMutation,
|
|
useUpdateLicenseTemplateMutation,
|
|
usePublishLicenseTemplateMutation,
|
|
useArchiveLicenseTemplateMutation,
|
|
useDeleteLicenseTemplateMutation,
|
|
useGetDocumentReviewsQuery,
|
|
useReviewDocumentMutation,
|
|
useClearDocumentReviewMutation,
|
|
useGetSavedViewsQuery,
|
|
useCreateSavedViewMutation,
|
|
useDeleteSavedViewMutation,
|
|
useLazyExportApplicationsQuery,
|
|
useGetAssignableOfficersQuery,
|
|
useSuspendLicenseMutation,
|
|
useRevokeLicenseMutation,
|
|
useReinstateLicenseMutation,
|
|
useAssignApplicationMutation,
|
|
useHoldApplicationMutation,
|
|
useResumeApplicationMutation,
|
|
useEscalateApplicationMutation,
|
|
useGetApplicationForReviewQuery,
|
|
useClaimApplicationMutation,
|
|
useCompleteReviewMutation,
|
|
useRequestAdjustmentMutation,
|
|
useApproveDocumentsMutation,
|
|
useFinalApproveMutation,
|
|
useRejectApplicationMutation,
|
|
useScheduleExamMutation,
|
|
useRequestExamPaymentMutation,
|
|
useConfirmPaymentMutation,
|
|
useScheduleInspectionMutation,
|
|
useGetInspectionsQuery,
|
|
useRecordInspectionResultMutation,
|
|
useGetNotificationsQuery,
|
|
useGetUnseenNotificationsQuery,
|
|
useMarkNotificationReadMutation,
|
|
useGetMyOperatorTypesQuery,
|
|
useUpdateMyOperatorTypesMutation,
|
|
} = licensingApi;
|