mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-28 18:20:59 +00:00
1097 lines
39 KiB
TypeScript
1097 lines
39 KiB
TypeScript
import { baseApi } from '../../base-api';
|
|
import type {
|
|
AppNotification,
|
|
ApplicationDetail,
|
|
ApplicationKind,
|
|
ApplicationPayment,
|
|
ApplicationStaff,
|
|
Attachment,
|
|
Department,
|
|
DocumentRequirement,
|
|
FormSchemaPalette,
|
|
FormSectionConfig,
|
|
InitiatePaymentResult,
|
|
IssuedLicense,
|
|
Inspection,
|
|
LicenseApplication,
|
|
LicenseCategoryDefinition,
|
|
LicenseStatus,
|
|
LicenseType,
|
|
LicenseTypeRequirements,
|
|
OperatorType,
|
|
AssignableOfficer,
|
|
DocumentDecision,
|
|
DocumentReview,
|
|
EligibleExam,
|
|
ExportResult,
|
|
LicenseTemplate,
|
|
Paginated,
|
|
QueueCounts,
|
|
QueueFilter,
|
|
Rank,
|
|
RankCertificateCategory,
|
|
RemarkTargetType,
|
|
SavedQueueView,
|
|
SchemaIssue,
|
|
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',
|
|
'DocumentRequirement',
|
|
'Department',
|
|
'Rank',
|
|
] 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)],
|
|
}),
|
|
|
|
// ------------------------------------------------ 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')]),
|
|
}),
|
|
|
|
// --------------------------------------------------- departments & ranks
|
|
/** Every department, for the admin editor. */
|
|
getDepartments: builder.query<Paginated<Department>, void>({
|
|
query: () => ({ url: '/departments' }),
|
|
providesTags: () => [listTag('Department')],
|
|
}),
|
|
|
|
/** Active departments only — the applicant-facing picker. */
|
|
getActiveDepartments: builder.query<Department[], void>({
|
|
query: () => ({ url: '/departments/active/list' }),
|
|
providesTags: () => [listTag('Department')],
|
|
}),
|
|
|
|
createDepartment: builder.mutation<
|
|
Department,
|
|
Partial<Department> & { code: string; name: Department['name'] }
|
|
>({
|
|
query: (body) => ({ url: '/departments', method: 'POST', body }),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('Department')]),
|
|
}),
|
|
|
|
updateDepartment: builder.mutation<Department, { id: string } & Partial<Department>>({
|
|
query: ({ id, ...body }) => ({ url: `/departments/${id}`, method: 'PUT', body }),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('Department')]),
|
|
}),
|
|
|
|
deleteDepartment: builder.mutation<unknown, string>({
|
|
query: (id) => ({ url: `/departments/${id}`, method: 'DELETE' }),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('Department')]),
|
|
}),
|
|
|
|
/** Every rank, for the admin editor to filter/group by department client-side. */
|
|
getRanks: builder.query<Paginated<Rank>, void>({
|
|
query: () => ({ url: '/ranks' }),
|
|
providesTags: () => [listTag('Rank')],
|
|
}),
|
|
|
|
/** One department's ladder for a category, ordered — the applicant wizard's rank picker. */
|
|
getRankLadder: builder.query<
|
|
Rank[],
|
|
{ departmentId: string; certificateCategory: RankCertificateCategory }
|
|
>({
|
|
query: (params) => ({ url: '/ranks/ladder', params }),
|
|
providesTags: () => [listTag('Rank')],
|
|
}),
|
|
|
|
createRank: builder.mutation<
|
|
Rank,
|
|
Partial<Rank> & {
|
|
departmentId: string;
|
|
certificateCategory: RankCertificateCategory;
|
|
key: string;
|
|
name: Rank['name'];
|
|
}
|
|
>({
|
|
query: (body) => ({ url: '/ranks', method: 'POST', body }),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('Rank')]),
|
|
}),
|
|
|
|
updateRank: builder.mutation<Rank, { id: string } & Partial<Rank>>({
|
|
query: ({ id, ...body }) => ({ url: `/ranks/${id}`, method: 'PUT', body }),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('Rank')]),
|
|
}),
|
|
|
|
deleteRank: builder.mutation<unknown, string>({
|
|
query: (id) => ({ url: `/ranks/${id}`, method: 'DELETE' }),
|
|
invalidatesTags: (_r, error) => (error ? [] : [listTag('Rank')]),
|
|
}),
|
|
|
|
// -------------------------------------------------------- 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, string | void>({
|
|
query: (licenseTypeKey) => ({
|
|
url: '/license-application-review/counts',
|
|
params: licenseTypeKey ? { licenseTypeKey } : undefined,
|
|
}),
|
|
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')],
|
|
}),
|
|
|
|
/**
|
|
* Exam sittings valid for this application's rank — what the
|
|
* schedule-exam picker offers, instead of every exam in the system.
|
|
*/
|
|
getEligibleExams: builder.query<EligibleExam[], string>({
|
|
query: (id) => ({ url: `/license-application-review/${id}/eligible-exams` }),
|
|
providesTags: (_r, _e, id) => [itemTag('LicenseApplication', id)],
|
|
}),
|
|
|
|
/** 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, examDate: _examDate, ...body }) => ({
|
|
// Matches the controller's `:id/exam-scheduled` route — `examDate`
|
|
// is UI-only context for the confirmation toast, not part of
|
|
// `MarkExamScheduledDto`, so it never goes on the wire.
|
|
url: `/license-application-review/${id}/exam-scheduled`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
/** Records a published examination result (pass or fail). */
|
|
recordExamOutcome: builder.mutation<
|
|
LicenseApplication,
|
|
{ id: string; passed: boolean; score?: number }
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-application-review/${id}/exam-outcome`,
|
|
method: 'POST',
|
|
body,
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
/**
|
|
* A failed candidate asks for another sitting. Re-opens the examination
|
|
* fee (EXAM_FAILED -> EXAM_PAYMENT_PENDING); eligibility was already
|
|
* assessed and paid for on the first attempt.
|
|
*/
|
|
retakeExam: builder.mutation<LicenseApplication, string>({
|
|
query: (id) => ({
|
|
url: `/license-applications/${id}/retake`,
|
|
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;
|
|
/** Scopes the draft to one rank's certificate. Omit for the type's default design. */
|
|
rankId?: string | null;
|
|
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;
|
|
rankId?: string | null;
|
|
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')],
|
|
}),
|
|
|
|
/**
|
|
* Starts the review: the team leader hands the file to an employee.
|
|
* `assign` above only re-points an application already in flight.
|
|
*/
|
|
assignReviewer: builder.mutation<
|
|
LicenseApplication,
|
|
{ id: string; officerId: string; remark?: string }
|
|
>({
|
|
query: ({ id, ...body }) => ({
|
|
url: `/license-application-review/${id}/assign-reviewer`,
|
|
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')],
|
|
}),
|
|
|
|
scheduleIssuance: builder.mutation<
|
|
LicenseApplication,
|
|
{ id: string; scheduledDate: string }
|
|
>({
|
|
query: ({ id, scheduledDate }) => ({
|
|
url: `/license-application-review/${id}/schedule-issuance`,
|
|
method: 'POST',
|
|
body: { scheduledDate },
|
|
}),
|
|
invalidatesTags: (_r, error, { id }) =>
|
|
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
|
}),
|
|
|
|
issueCertificate: builder.mutation<LicenseApplication, string>({
|
|
query: (id) => ({
|
|
url: `/license-application-review/${id}/issue-certificate`,
|
|
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,
|
|
useUpdateFormSchemaMutation,
|
|
useValidateFormSchemaMutation,
|
|
useGetFormSchemaPaletteQuery,
|
|
useGetDocumentRequirementsQuery,
|
|
useCreateDocumentRequirementMutation,
|
|
useUpdateDocumentRequirementMutation,
|
|
useDeleteDocumentRequirementMutation,
|
|
useGetDepartmentsQuery,
|
|
useGetActiveDepartmentsQuery,
|
|
useCreateDepartmentMutation,
|
|
useUpdateDepartmentMutation,
|
|
useDeleteDepartmentMutation,
|
|
useGetRanksQuery,
|
|
useGetRankLadderQuery,
|
|
useCreateRankMutation,
|
|
useUpdateRankMutation,
|
|
useDeleteRankMutation,
|
|
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,
|
|
useAssignReviewerMutation,
|
|
useHoldApplicationMutation,
|
|
useResumeApplicationMutation,
|
|
useEscalateApplicationMutation,
|
|
useGetApplicationForReviewQuery,
|
|
useClaimApplicationMutation,
|
|
useCompleteReviewMutation,
|
|
useRequestAdjustmentMutation,
|
|
useApproveDocumentsMutation,
|
|
useFinalApproveMutation,
|
|
useRejectApplicationMutation,
|
|
useGetEligibleExamsQuery,
|
|
useScheduleExamMutation,
|
|
useRecordExamOutcomeMutation,
|
|
useRetakeExamMutation,
|
|
useConfirmPaymentMutation,
|
|
useScheduleIssuanceMutation,
|
|
useIssueCertificateMutation,
|
|
useScheduleInspectionMutation,
|
|
useGetInspectionsQuery,
|
|
useRecordInspectionResultMutation,
|
|
useGetNotificationsQuery,
|
|
useGetUnseenNotificationsQuery,
|
|
useMarkNotificationReadMutation,
|
|
useGetMyOperatorTypesQuery,
|
|
useUpdateMyOperatorTypesMutation,
|
|
} = licensingApi;
|