Merge branch 'WorkflowChange' of https://github.com/Tria-plc/emaui into estif-branch-1

This commit is contained in:
Estifo77
2026-08-21 08:55:24 +03:00
29 changed files with 2092 additions and 1157 deletions

View File

@@ -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' } },

View File

@@ -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,

View File

@@ -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',

View File

@@ -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 {