mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-09-07 20:05:42 +00:00
@@ -10,7 +10,7 @@ import { resolveSessionContext } from "../session";
|
||||
export const BASE_API_URL =
|
||||
(import.meta as { env?: Record<string, string> }).env?.[
|
||||
"VITE_BASE_API_URL"
|
||||
]?.trim() || "http://localhost:3000/api";
|
||||
]?.trim() || "https://ema-api-dev.triaplc.com/api";
|
||||
|
||||
let _onTokenExpired: (() => Promise<string>) | null = null;
|
||||
let _onAuthFailure: (() => void) | null = null;
|
||||
|
||||
@@ -74,6 +74,51 @@ const handlers: MockHandler[] = [
|
||||
pattern: /^\/license-types$/,
|
||||
respond: () => paginated(clone(mockLicenseTypes)),
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
pattern: /^\/license-types$/,
|
||||
respond: (req) => {
|
||||
const body = (req.body ?? {}) as Record<string, unknown>;
|
||||
const key = String(body.key ?? '').trim().toUpperCase();
|
||||
if (mockLicenseTypes.some((t) => t.key === key)) return undefined;
|
||||
const created = {
|
||||
id: `lt-${key.toLowerCase()}`,
|
||||
category: 'MARITIME_PERSONNEL',
|
||||
familyKind: 'LOGISTICS_LICENSE',
|
||||
serviceKind: 'LICENSE',
|
||||
workflowProfile: 'STANDARD',
|
||||
feeNewApplication: null,
|
||||
feeRenewal: null,
|
||||
feeCurrency: 'ETB',
|
||||
capitalThreshold: null,
|
||||
validityMonths: 12,
|
||||
slaHours: null,
|
||||
inspectionRequired: true,
|
||||
issuesCertificate: true,
|
||||
renewalEnabled: true,
|
||||
requiresIssuanceScheduling: false,
|
||||
requiresOperatorMode: true,
|
||||
formSchema: { sections: [] },
|
||||
isActive: true,
|
||||
sortOrder: mockLicenseTypes.length,
|
||||
...body,
|
||||
key,
|
||||
};
|
||||
(mockLicenseTypes as unknown[]).push(created);
|
||||
return clone(created);
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'PATCH',
|
||||
pattern: /^\/license-types\/([\w-]+)\/status$/,
|
||||
respond: (req, match) => {
|
||||
const found = mockLicenseTypes.find((t) => t.id === match[1]);
|
||||
if (!found) return undefined;
|
||||
const body = (req.body ?? {}) as { isActive?: boolean };
|
||||
(found as { isActive: boolean }).isActive = body.isActive ?? found.isActive;
|
||||
return clone(found);
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
pattern: /^\/license-types\/requirements\/([\w-]+)$/,
|
||||
|
||||
@@ -402,6 +402,7 @@ export const mockApplicationDetails: Record<string, any> = Object.fromEntries(
|
||||
remark: 'Ownership document is blurred — please re-upload a clearer scan.',
|
||||
isResolved: false,
|
||||
resolvedAt: null,
|
||||
valueChangedAt: null,
|
||||
createdAt: application.submittedAt ?? application.createdAt,
|
||||
},
|
||||
]
|
||||
@@ -417,6 +418,7 @@ export const mockApplicationDetails: Record<string, any> = Object.fromEntries(
|
||||
remark: 'Ownership document is blurred — please re-upload a clearer scan.',
|
||||
isResolved: false,
|
||||
resolvedAt: null,
|
||||
valueChangedAt: null,
|
||||
createdAt: application.submittedAt ?? application.createdAt,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { baseApi } from '../../base-api';
|
||||
import type {
|
||||
AdminDashboardAnalytics,
|
||||
ApplicantDashboardSummary,
|
||||
ExamStateView,
|
||||
AppNotification,
|
||||
ApplicationDetail,
|
||||
@@ -9,6 +11,7 @@ import type {
|
||||
Attachment,
|
||||
Department,
|
||||
DocumentRequirement,
|
||||
FamilyKind,
|
||||
FormSchemaPalette,
|
||||
FormSectionConfig,
|
||||
InitiatePaymentResult,
|
||||
@@ -19,6 +22,7 @@ import type {
|
||||
LicenseCategoryDefinition,
|
||||
LicenseStatus,
|
||||
LicenseType,
|
||||
LicenseTypeCreate,
|
||||
LicenseTypeRequirements,
|
||||
OperatorType,
|
||||
AssignableOfficer,
|
||||
@@ -173,6 +177,38 @@ export const licensingApi = baseApi
|
||||
providesTags: () => [listTag('LicenseType')],
|
||||
}),
|
||||
|
||||
/**
|
||||
* Creates the licence type row.
|
||||
*
|
||||
* The one write on this resource with no screen until now: everything
|
||||
* after creation — form, documents, staff rules, fees, validity,
|
||||
* certificate design — had an editor, but the type itself had to be
|
||||
* added by seed or by calling the API directly.
|
||||
*/
|
||||
createLicenseType: builder.mutation<LicenseType, LicenseTypeCreate>({
|
||||
query: (body) => ({ url: '/license-types', method: 'POST', body }),
|
||||
invalidatesTags: (_r, error) => (error ? [] : [listTag('LicenseType')]),
|
||||
}),
|
||||
|
||||
/**
|
||||
* Replaces the row's own fields. The narrow routes (`/fees`,
|
||||
* `/validity`, `/behavior`, `/form-schema`) are still the way to change
|
||||
* what they own; this is the catalogue entry itself — key, names,
|
||||
* category, family kind, prefix, sort order, active.
|
||||
*/
|
||||
updateLicenseType: builder.mutation<
|
||||
LicenseType,
|
||||
{ id: string } & Partial<LicenseTypeCreate>
|
||||
>({
|
||||
query: ({ id, ...body }) => ({
|
||||
url: `/license-types/${id}`,
|
||||
method: 'PUT',
|
||||
body,
|
||||
}),
|
||||
invalidatesTags: (_r, error, { id }) =>
|
||||
error ? [] : [listTag('LicenseType'), itemTag('LicenseType', id)],
|
||||
}),
|
||||
|
||||
/**
|
||||
* Fee configuration, for licensing administrators.
|
||||
*
|
||||
@@ -222,6 +258,7 @@ export const licensingApi = baseApi
|
||||
id: string;
|
||||
workflowProfile?: WorkflowProfile;
|
||||
serviceKind?: ServiceKind;
|
||||
familyKind?: FamilyKind;
|
||||
completionEffect?: CompletionEffect | null;
|
||||
certificateCategory?: CertificateCategory | null;
|
||||
requiresExamination?: boolean;
|
||||
@@ -252,6 +289,26 @@ export const licensingApi = baseApi
|
||||
error ? [] : [listTag('LicenseType'), itemTag('LicenseType', id)],
|
||||
}),
|
||||
|
||||
/**
|
||||
* Opens or closes a type to new applications. Applications already in
|
||||
* flight hold the type by id and keep running; only the portal
|
||||
* catalogue changes. Closing also notifies every holder, applicant and
|
||||
* drafter of the type, and freezes their drafts — server-side, so the
|
||||
* same happens whichever client flips it.
|
||||
*/
|
||||
updateLicenseStatus: builder.mutation<
|
||||
LicenseType,
|
||||
{ id: string; isActive: boolean }
|
||||
>({
|
||||
query: ({ id, isActive }) => ({
|
||||
url: `/license-types/${id}/status`,
|
||||
method: 'PATCH',
|
||||
body: { isActive },
|
||||
}),
|
||||
invalidatesTags: (_r, error, { id }) =>
|
||||
error ? [] : [itemTag('LicenseType', id), listTag('LicenseType')],
|
||||
}),
|
||||
|
||||
/** Validity is edited beside the certificate design, not with the fees. */
|
||||
updateLicenseValidity: builder.mutation<
|
||||
LicenseType,
|
||||
@@ -492,6 +549,11 @@ export const licensingApi = baseApi
|
||||
providesTags: () => [listTag('LicenseApplication')],
|
||||
}),
|
||||
|
||||
getMyDashboardSummary: builder.query<ApplicantDashboardSummary, void>({
|
||||
query: () => ({ url: '/license-applications/mine/summary' }),
|
||||
providesTags: () => [listTag('LicenseApplication'), listTag('License')],
|
||||
}),
|
||||
|
||||
getApplication: builder.query<ApplicationDetail, string>({
|
||||
query: (id) => ({ url: `/license-applications/${id}` }),
|
||||
providesTags: (_r, _e, id) => [itemTag('LicenseApplication', id)],
|
||||
@@ -746,6 +808,17 @@ export const licensingApi = baseApi
|
||||
providesTags: () => [listTag('ApplicationQueue')],
|
||||
}),
|
||||
|
||||
getAdminDashboardAnalytics: builder.query<
|
||||
AdminDashboardAnalytics,
|
||||
{ period?: string } | void
|
||||
>({
|
||||
query: (args) => ({
|
||||
url: '/license-application-review/dashboard-analytics',
|
||||
params: args?.period ? { period: args.period } : undefined,
|
||||
}),
|
||||
providesTags: () => [listTag('ApplicationQueue'), listTag('License')],
|
||||
}),
|
||||
|
||||
getApplicationForReview: builder.query<ApplicationDetail, string>({
|
||||
query: (id) => ({ url: `/license-application-review/${id}` }),
|
||||
providesTags: (_r, _e, id) => [itemTag('LicenseApplication', id)],
|
||||
@@ -1364,6 +1437,8 @@ export const licensingApi = baseApi
|
||||
export const {
|
||||
useGetLicenseTypesQuery,
|
||||
useGetLicenseCategoriesQuery,
|
||||
useCreateLicenseTypeMutation,
|
||||
useUpdateLicenseTypeMutation,
|
||||
useUpdateLicenseFeesMutation,
|
||||
useUpdateLicenseBehaviorMutation,
|
||||
useUpdateFormSchemaMutation,
|
||||
@@ -1389,11 +1464,13 @@ export const {
|
||||
useUpdateRankMutation,
|
||||
useDeleteRankMutation,
|
||||
useUpdateLicenseValidityMutation,
|
||||
useUpdateLicenseStatusMutation,
|
||||
useGetLicenseTypeRequirementsQuery,
|
||||
useCreateApplicationMutation,
|
||||
useGetExamStateForApplicationQuery,
|
||||
useDiscardApplicationMutation,
|
||||
useGetMyApplicationsQuery,
|
||||
useGetMyDashboardSummaryQuery,
|
||||
useGetApplicationQuery,
|
||||
useInitiatePaymentMutation,
|
||||
useBypassPaymentMutation,
|
||||
@@ -1417,6 +1494,7 @@ export const {
|
||||
useGetAssignedToMeQuery,
|
||||
useGetAllApplicationsQuery,
|
||||
useGetQueueCountsQuery,
|
||||
useGetAdminDashboardAnalyticsQuery,
|
||||
useGetLicenseTemplatesQuery,
|
||||
useGetTemplateVariablesQuery,
|
||||
useGetBuiltInTemplateQuery,
|
||||
|
||||
@@ -411,6 +411,18 @@ const ERROR_MESSAGES: Record<string, string> = {
|
||||
'Candidates are currently waiting to pay this fee. Removing it would leave them unable to pay and unable to continue — set a different amount instead.',
|
||||
form_schema_missing_protected_paths:
|
||||
'The form for this licence type does not contain the field this setting depends on. Add the field to the form first.',
|
||||
license_type_key_taken:
|
||||
'A licence type with this key already exists (it may be archived). Choose a different key.',
|
||||
license_type_key_in_use:
|
||||
'Applications already reference this licence type, so its key can no longer be changed.',
|
||||
invalid_form_schema:
|
||||
'The form schema is not consistent — a condition or field reference points at something the form does not define.',
|
||||
|
||||
// Exam certification catalogue.
|
||||
rank_not_found:
|
||||
'That STCW rank is not in the ranks registry, or has been switched off. Pick a rank from the list.',
|
||||
certification_in_use:
|
||||
'Exam sessions or question-bank items still use this certification. Reassign or remove those first.',
|
||||
};
|
||||
|
||||
export function extractErrorMessage(error: unknown, fallback = 'Something went wrong'): string {
|
||||
|
||||
@@ -277,6 +277,39 @@ export interface LicenseType {
|
||||
prerequisiteLicenseKeys?: string[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Body of `POST /license-types` — the row itself, before behaviour, fees, the
|
||||
* form or its document slots are configured.
|
||||
*
|
||||
* Mirrors `CreateLicenseTypeDto` field for field. Create the type inactive: an
|
||||
* active type with no form schema and no document requirements is immediately
|
||||
* visible to applicants.
|
||||
*/
|
||||
export interface LicenseTypeCreate {
|
||||
key: string;
|
||||
name: Bilingual;
|
||||
description?: Bilingual;
|
||||
category?: LicenseCategory;
|
||||
/** Omitted, the server derives it from `key` — see `FamilyKind`. */
|
||||
familyKind?: FamilyKind;
|
||||
/** Licence or registration; splits the catalogues without splitting storage. */
|
||||
serviceKind?: ServiceKind;
|
||||
/** Which transition set the application runs. */
|
||||
workflowProfile?: WorkflowProfile;
|
||||
certificatePrefix: string;
|
||||
feeNewApplication?: number | null;
|
||||
feeRenewal?: number | null;
|
||||
feeCurrency?: string;
|
||||
capitalThreshold?: number | null;
|
||||
validityMonths?: number;
|
||||
inspectionRequired?: boolean;
|
||||
issuesCertificate?: boolean;
|
||||
renewalEnabled?: boolean;
|
||||
formSchema?: { sections: FormSectionConfig[] };
|
||||
sortOrder?: number;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
/** What kind of document a certificate type produces. */
|
||||
export type CertificateCategory =
|
||||
"COC" | "COP" | "ENDORSEMENT" | "GOC" | "NATIONAL";
|
||||
@@ -487,6 +520,15 @@ export interface ApplicationRemark {
|
||||
remark: string;
|
||||
isResolved: boolean;
|
||||
resolvedAt: string | null;
|
||||
/**
|
||||
* When the applicant actually changed the value this remark points at.
|
||||
*
|
||||
* `resolvedAt` only says the item was ticked off — the portal bulk-resolves
|
||||
* every remark of the round right before resubmitting, so it proves nothing.
|
||||
* This is stamped by the section save or document re-upload itself, which
|
||||
* makes it the only trustworthy answer to "what did they actually correct?".
|
||||
*/
|
||||
valueChangedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -960,3 +1002,121 @@ export interface ExamStateView {
|
||||
/** Whether the exam engine produced the mark, or a person did. */
|
||||
autoGraded: boolean | null;
|
||||
}
|
||||
|
||||
export interface ApplicantDashboardSummary {
|
||||
counts: {
|
||||
totalApplications: number;
|
||||
activeLicenses: number;
|
||||
expiringSoonLicenses: number;
|
||||
pendingApplications: number;
|
||||
approvedApplications: number;
|
||||
draftApplications: number;
|
||||
actionRequiredApplications: number;
|
||||
};
|
||||
statusDistribution: Array<{
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}>;
|
||||
monthlyTrend: Array<{
|
||||
month: string;
|
||||
year: number;
|
||||
submitted: number;
|
||||
approved: number;
|
||||
}>;
|
||||
categoryBreakdown: Array<{
|
||||
category: string;
|
||||
count: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AdminDashboardAnalytics {
|
||||
kpis: {
|
||||
totalApplications: number;
|
||||
unclaimedQueue: number;
|
||||
assignedToMe: number;
|
||||
inProgressTotal: number;
|
||||
needsApplicant: number;
|
||||
awaitingPayment: number;
|
||||
approved: number;
|
||||
rejected: number;
|
||||
overdueSla: number;
|
||||
withinSla: number;
|
||||
slaComplianceRate: number;
|
||||
activeLicenses: number;
|
||||
revenue: {
|
||||
totalCollected: number;
|
||||
pendingAmount: number;
|
||||
currency: string;
|
||||
};
|
||||
};
|
||||
statusDistribution: Array<{
|
||||
name: string;
|
||||
value: number;
|
||||
color: string;
|
||||
}>;
|
||||
monthlyTrend: Array<{
|
||||
month: string;
|
||||
year: number;
|
||||
submitted: number;
|
||||
approved: number;
|
||||
rejected: number;
|
||||
revenue: number;
|
||||
}>;
|
||||
categoryBreakdown: Array<{
|
||||
category: string;
|
||||
count: number;
|
||||
percentage: number;
|
||||
}>;
|
||||
departmentSummary: {
|
||||
seafarerRegistrations: {
|
||||
total: number;
|
||||
submitted: number;
|
||||
approved: number;
|
||||
};
|
||||
seamanBooks: {
|
||||
total: number;
|
||||
pending: number;
|
||||
issued: number;
|
||||
};
|
||||
btc: {
|
||||
total: number;
|
||||
pending: number;
|
||||
issued: number;
|
||||
};
|
||||
vessels: {
|
||||
total: number;
|
||||
active: number;
|
||||
pending: number;
|
||||
};
|
||||
};
|
||||
oldestUnclaimed: Array<{
|
||||
id: string;
|
||||
applicationNumber: string;
|
||||
companyName: string;
|
||||
licenseTypeName: string;
|
||||
category: string;
|
||||
status: string;
|
||||
submittedAt: string;
|
||||
slaHours: number | null;
|
||||
}>;
|
||||
urgentApplications: Array<{
|
||||
id: string;
|
||||
applicationNumber: string;
|
||||
companyName: string | null;
|
||||
licenseTypeName: string;
|
||||
category: string;
|
||||
submittedAt: string | null;
|
||||
hoursLeft: number;
|
||||
isOverdue: boolean;
|
||||
}>;
|
||||
officerWorkload: Array<{
|
||||
officerId: string;
|
||||
officerName: string;
|
||||
activeCount: number;
|
||||
overdueCount: number;
|
||||
onScheduleCount: number;
|
||||
}>;
|
||||
period?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,17 @@ export interface SeafarerDocument {
|
||||
issuedAt: string | null;
|
||||
rejectionReason: string | null;
|
||||
createdAt: string;
|
||||
/**
|
||||
* Policy from the document's licence-type row, as configured on the
|
||||
* backoffice Behaviour tab. Present on `/seafarer-documents/mine` so the
|
||||
* portal shows the pickup step only for a type that has one, and offers
|
||||
* Renew / Replace only inside the configured window — the same flags
|
||||
* `/licenses/mine` carries for licences. Absent on older servers.
|
||||
*/
|
||||
requiresIssuanceScheduling?: boolean;
|
||||
daysUntilExpiry?: number | null;
|
||||
renewable?: boolean;
|
||||
reissuable?: boolean;
|
||||
}
|
||||
|
||||
export interface SeafarerDocumentApplicant {
|
||||
|
||||
Reference in New Issue
Block a user