mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-29 16:28:13 +00:00
Initial End to End functionality
This commit is contained in:
@@ -14,10 +14,44 @@ import type {
|
||||
LicenseStatus,
|
||||
LicenseType,
|
||||
LicenseTypeRequirements,
|
||||
AssignableOfficer,
|
||||
DocumentDecision,
|
||||
DocumentReview,
|
||||
ExportResult,
|
||||
LicenseTemplate,
|
||||
Paginated,
|
||||
QueueCounts,
|
||||
QueueFilter,
|
||||
RemarkTargetType,
|
||||
SavedQueueView,
|
||||
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',
|
||||
'LicenseApplication',
|
||||
@@ -26,6 +60,8 @@ const TAGS = [
|
||||
'Notification',
|
||||
'Inspection',
|
||||
'License',
|
||||
'SavedView',
|
||||
'LicenseTemplate',
|
||||
] as const;
|
||||
|
||||
const listTag = (type: (typeof TAGS)[number]) => ({ type, id: 'LIST' }) as const;
|
||||
@@ -86,6 +122,20 @@ export const licensingApi = baseApi
|
||||
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 }
|
||||
@@ -234,30 +284,82 @@ export const licensingApi = baseApi
|
||||
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>,
|
||||
{ licenseTypeId?: string; search?: string } | void
|
||||
>({
|
||||
query: (params) => ({ url: '/license-application-review/queue', params: params ?? {} }),
|
||||
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>,
|
||||
{ licenseTypeId?: string; search?: string } | void
|
||||
QueueFilter | void
|
||||
>({
|
||||
query: (params) => ({
|
||||
url: '/license-application-review/assigned-to-me',
|
||||
params: params ?? {},
|
||||
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)],
|
||||
@@ -288,6 +390,8 @@ export const licensingApi = baseApi
|
||||
id: string;
|
||||
generalRemark?: string;
|
||||
items: { targetType: RemarkTargetType; targetKey: string; remark: string }[];
|
||||
/** Officer-edited wording for the applicant notification. */
|
||||
notificationBody?: string;
|
||||
}
|
||||
>({
|
||||
query: ({ id, ...body }) => ({
|
||||
@@ -327,16 +431,213 @@ export const licensingApi = baseApi
|
||||
error ? [] : [itemTag('LicenseApplication', id), listTag('ApplicationQueue')],
|
||||
}),
|
||||
|
||||
rejectApplication: builder.mutation<LicenseApplication, { id: string; reason: string }>({
|
||||
query: ({ id, reason }) => ({
|
||||
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')],
|
||||
}),
|
||||
|
||||
// ------------------------------------------------- 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;
|
||||
}
|
||||
>({
|
||||
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`,
|
||||
@@ -397,6 +698,7 @@ export const {
|
||||
useGetLicenseTypesQuery,
|
||||
useGetLicenseCategoriesQuery,
|
||||
useUpdateLicenseFeesMutation,
|
||||
useUpdateLicenseValidityMutation,
|
||||
useGetLicenseTypeRequirementsQuery,
|
||||
useCreateApplicationMutation,
|
||||
useGetMyApplicationsQuery,
|
||||
@@ -417,6 +719,31 @@ export const {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user