mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
Merge branch 'WorkflowChange' of https://github.com/Tria-plc/emaui into estif-branch-1
This commit is contained in:
@@ -337,7 +337,7 @@ export const mockApplications: Record<string, any> = {
|
||||
licenseType: { id: 'license-type-coc', key: 'CERTIFICATE_OF_COMPETENCY', name: { en: 'Certificate of Competency' } },
|
||||
applicantUserId: 'user-mock-001',
|
||||
kind: 'RENEWAL',
|
||||
status: 'ELIGIBILITY_APPROVED',
|
||||
status: 'ELIGIBILITY_PAID',
|
||||
assignedOfficerId: 'officer-mock-002',
|
||||
claimedAt: '2026-08-01T10:00:00.000Z',
|
||||
formData: { account: { applicantName: 'Abebe Tesfaye' } },
|
||||
|
||||
@@ -6,6 +6,9 @@ import type {
|
||||
ApplicationPayment,
|
||||
ApplicationStaff,
|
||||
Attachment,
|
||||
DocumentRequirement,
|
||||
FormSchemaPalette,
|
||||
FormSectionConfig,
|
||||
InitiatePaymentResult,
|
||||
IssuedLicense,
|
||||
Inspection,
|
||||
@@ -25,6 +28,7 @@ import type {
|
||||
QueueFilter,
|
||||
RemarkTargetType,
|
||||
SavedQueueView,
|
||||
SchemaIssue,
|
||||
TemplateFieldPlacement,
|
||||
TemplateLogoPlacement,
|
||||
TemplatePageOptions,
|
||||
@@ -66,6 +70,7 @@ const TAGS = [
|
||||
'License',
|
||||
'SavedView',
|
||||
'LicenseTemplate',
|
||||
'DocumentRequirement',
|
||||
] as const;
|
||||
|
||||
const listTag = (type: (typeof TAGS)[number]) => ({ type, id: 'LIST' }) as const;
|
||||
@@ -178,6 +183,76 @@ export const licensingApi = baseApi
|
||||
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')]),
|
||||
}),
|
||||
|
||||
// -------------------------------------------------------- application
|
||||
createApplication: builder.mutation<
|
||||
LicenseApplication,
|
||||
@@ -494,9 +569,26 @@ export const licensingApi = baseApi
|
||||
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}/schedule-exam`,
|
||||
url: `/license-application-review/${id}/exam-outcome`,
|
||||
method: 'POST',
|
||||
body,
|
||||
}),
|
||||
@@ -505,12 +597,13 @@ export const licensingApi = baseApi
|
||||
}),
|
||||
|
||||
/**
|
||||
* Raises the examination fee — after eligibility approval, or again when
|
||||
* a failed candidate elects to resit.
|
||||
* 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.
|
||||
*/
|
||||
requestExamPayment: builder.mutation<LicenseApplication, string>({
|
||||
retakeExam: builder.mutation<LicenseApplication, string>({
|
||||
query: (id) => ({
|
||||
url: `/license-applications/${id}/request-exam-payment`,
|
||||
url: `/license-applications/${id}/retake`,
|
||||
method: 'POST',
|
||||
}),
|
||||
invalidatesTags: (_r, error, id) =>
|
||||
@@ -809,6 +902,13 @@ export const {
|
||||
useGetLicenseTypesQuery,
|
||||
useGetLicenseCategoriesQuery,
|
||||
useUpdateLicenseFeesMutation,
|
||||
useUpdateFormSchemaMutation,
|
||||
useValidateFormSchemaMutation,
|
||||
useGetFormSchemaPaletteQuery,
|
||||
useGetDocumentRequirementsQuery,
|
||||
useCreateDocumentRequirementMutation,
|
||||
useUpdateDocumentRequirementMutation,
|
||||
useDeleteDocumentRequirementMutation,
|
||||
useUpdateLicenseValidityMutation,
|
||||
useGetLicenseTypeRequirementsQuery,
|
||||
useCreateApplicationMutation,
|
||||
@@ -864,7 +964,8 @@ export const {
|
||||
useFinalApproveMutation,
|
||||
useRejectApplicationMutation,
|
||||
useScheduleExamMutation,
|
||||
useRequestExamPaymentMutation,
|
||||
useRecordExamOutcomeMutation,
|
||||
useRetakeExamMutation,
|
||||
useConfirmPaymentMutation,
|
||||
useScheduleIssuanceMutation,
|
||||
useIssueCertificateMutation,
|
||||
|
||||
@@ -75,7 +75,8 @@ export const STATUS_LABELS: Record<LicenseStatus, string> = {
|
||||
SCHEDULED: 'Pickup Scheduled',
|
||||
CERTIFICATE_ISSUED: 'Certificate Issued',
|
||||
COMPLETED: 'Completed',
|
||||
ELIGIBILITY_APPROVED: 'Eligible to Sit',
|
||||
ELIGIBILITY_PAYMENT_PENDING: 'Eligibility Fee Due',
|
||||
ELIGIBILITY_PAID: 'Eligibility Under Review',
|
||||
EXAM_PAYMENT_PENDING: 'Exam Fee Due',
|
||||
EXAM_PAID: 'Awaiting Exam Date',
|
||||
EXAM_SCHEDULED: 'Exam Scheduled',
|
||||
@@ -101,7 +102,8 @@ export const STATUS_COLORS: Record<LicenseStatus, string> = {
|
||||
SCHEDULED: 'cyan',
|
||||
CERTIFICATE_ISSUED: 'green',
|
||||
COMPLETED: 'green',
|
||||
ELIGIBILITY_APPROVED: 'teal',
|
||||
ELIGIBILITY_PAYMENT_PENDING: 'yellow',
|
||||
ELIGIBILITY_PAID: 'lime',
|
||||
EXAM_PAYMENT_PENDING: 'yellow',
|
||||
EXAM_PAID: 'lime',
|
||||
EXAM_SCHEDULED: 'cyan',
|
||||
@@ -136,7 +138,8 @@ export const STATUS_PROGRESS: Record<LicenseStatus, number> = {
|
||||
REJECTED: 100,
|
||||
// The exam leg sits between approval and the certificate fee, so these
|
||||
// interleave with PAYMENT_PENDING (80) rather than running past it.
|
||||
ELIGIBILITY_APPROVED: 60,
|
||||
ELIGIBILITY_PAYMENT_PENDING: 52,
|
||||
ELIGIBILITY_PAID: 56,
|
||||
EXAM_PAYMENT_PENDING: 64,
|
||||
EXAM_PAID: 68,
|
||||
EXAM_SCHEDULED: 72,
|
||||
@@ -150,6 +153,9 @@ export const APPLICANT_ACTION_STATUSES: LicenseStatus[] = [
|
||||
'DRAFT',
|
||||
'RESUBMIT_REQUIRED',
|
||||
'PAYMENT_PENDING',
|
||||
// Due the moment an examined application is submitted, before any officer
|
||||
// looks at it.
|
||||
'ELIGIBILITY_PAYMENT_PENDING',
|
||||
// Both wait on the candidate: one to pay for a sitting, one to decide to
|
||||
// sit again after a failure.
|
||||
'EXAM_PAYMENT_PENDING',
|
||||
|
||||
@@ -41,9 +41,11 @@ export type LicenseStatus =
|
||||
| "SCHEDULED"
|
||||
| "CERTIFICATE_ISSUED"
|
||||
| "COMPLETED"
|
||||
// Examined certificates (CoC, some CoP): approval establishes eligibility,
|
||||
// the candidate pays to sit, and the certificate fee falls due on a pass.
|
||||
| "ELIGIBILITY_APPROVED"
|
||||
// Examined certificates (CoC, some CoP): the eligibility assessment fee is
|
||||
// due before review starts, then the candidate pays to sit, and the
|
||||
// certificate fee falls due on a pass.
|
||||
| "ELIGIBILITY_PAYMENT_PENDING"
|
||||
| "ELIGIBILITY_PAID"
|
||||
| "EXAM_PAYMENT_PENDING"
|
||||
| "EXAM_PAID"
|
||||
| "EXAM_SCHEDULED"
|
||||
@@ -77,10 +79,12 @@ export interface FormFieldConfig {
|
||||
label: Bilingual;
|
||||
type: FormFieldType;
|
||||
required?: boolean;
|
||||
placeholder?: Bilingual;
|
||||
helpText?: Bilingual;
|
||||
options?: { value: string; label: Bilingual }[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
maxLength?: number;
|
||||
showWhen?: FieldCondition;
|
||||
readOnly?: boolean;
|
||||
source?: string;
|
||||
@@ -233,6 +237,7 @@ export interface StcwCapacityRow {
|
||||
|
||||
export interface DocumentRequirement {
|
||||
id: string;
|
||||
licenseTypeId: string;
|
||||
key: string;
|
||||
name: Bilingual;
|
||||
description?: Bilingual;
|
||||
@@ -242,7 +247,32 @@ export interface DocumentRequirement {
|
||||
allowedMimeTypes: string[];
|
||||
maxSizeMb: number;
|
||||
requiresValidityDates: boolean;
|
||||
allowMultiple: boolean;
|
||||
sortOrder: number;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* What the form-schema builder may put on a form — the field types the engine
|
||||
* understands, which constraints each one honours, the condition operators it
|
||||
* supports, and the prefill sources a read-only field may draw from. Drives
|
||||
* the builder's pickers so they never hardcode a list the server already owns.
|
||||
*/
|
||||
export interface FormSchemaPalette {
|
||||
fieldTypes: {
|
||||
type: FormFieldType;
|
||||
supportsOptions: boolean;
|
||||
supportsRange: boolean;
|
||||
supportsMaxLength: boolean;
|
||||
}[];
|
||||
conditionOperators: ("equals" | "notEquals" | "in" | "isSet")[];
|
||||
prefillSources: string[];
|
||||
}
|
||||
|
||||
/** One problem the server's form-schema lint found. */
|
||||
export interface SchemaIssue {
|
||||
path: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface StaffEvidenceRequirement {
|
||||
|
||||
Reference in New Issue
Block a user