mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
Merge branch 'WorkflowChange' of https://github.com/Tria-plc/emaui into estif-branch-1
This commit is contained in:
@@ -724,6 +724,28 @@ export const licensingApi = baseApi
|
||||
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,
|
||||
@@ -844,6 +866,8 @@ export const {
|
||||
useScheduleExamMutation,
|
||||
useRequestExamPaymentMutation,
|
||||
useConfirmPaymentMutation,
|
||||
useScheduleIssuanceMutation,
|
||||
useIssueCertificateMutation,
|
||||
useScheduleInspectionMutation,
|
||||
useGetInspectionsQuery,
|
||||
useRecordInspectionResultMutation,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolveTokenFromStorage } from '../../session';
|
||||
import type {
|
||||
Bilingual,
|
||||
FamilyKind,
|
||||
FormFieldConfig,
|
||||
FormSectionConfig,
|
||||
LicenseApplication,
|
||||
@@ -69,6 +70,7 @@ export const STATUS_LABELS: Record<LicenseStatus, string> = {
|
||||
PAYMENT_PENDING: 'Payment Pending',
|
||||
PAID: 'Paid',
|
||||
PAYMENT_CONFIRMED: 'Preparing Certificate',
|
||||
SCHEDULED: 'Pickup Scheduled',
|
||||
CERTIFICATE_ISSUED: 'Certificate Issued',
|
||||
COMPLETED: 'Completed',
|
||||
ELIGIBILITY_APPROVED: 'Eligible to Sit',
|
||||
@@ -94,6 +96,7 @@ export const STATUS_COLORS: Record<LicenseStatus, string> = {
|
||||
PAYMENT_PENDING: 'yellow',
|
||||
PAID: 'lime',
|
||||
PAYMENT_CONFIRMED: 'teal',
|
||||
SCHEDULED: 'cyan',
|
||||
CERTIFICATE_ISSUED: 'green',
|
||||
COMPLETED: 'green',
|
||||
ELIGIBILITY_APPROVED: 'teal',
|
||||
@@ -125,6 +128,7 @@ export const STATUS_PROGRESS: Record<LicenseStatus, number> = {
|
||||
PAYMENT_PENDING: 80,
|
||||
PAID: 88,
|
||||
PAYMENT_CONFIRMED: 94,
|
||||
SCHEDULED: 97,
|
||||
CERTIFICATE_ISSUED: 100,
|
||||
COMPLETED: 100,
|
||||
REJECTED: 100,
|
||||
@@ -169,9 +173,87 @@ export const APPLICANT_NAME_TYPE_KEYS = [
|
||||
'ENDORSEMENT_GOC',
|
||||
];
|
||||
|
||||
/** Company name, or applicant name for licence types that have no company. */
|
||||
const FAMILY_KIND_BY_KEY: Partial<Record<string, FamilyKind>> = {
|
||||
SEAFARER_REGISTRATION: 'DOCUMENT',
|
||||
SEAMAN_BOOK: 'DOCUMENT',
|
||||
VESSEL_REGISTRATION: 'DOCUMENT',
|
||||
BTC_BASIC_TRAINING: 'CERTIFICATE',
|
||||
CERTIFICATE_OF_COMPETENCY: 'CERTIFICATE',
|
||||
CERTIFICATE_OF_PROFICIENCY: 'CERTIFICATE',
|
||||
ENDORSEMENT_COC: 'CERTIFICATE',
|
||||
ENDORSEMENT_GOC: 'CERTIFICATE',
|
||||
FREIGHT_FORWARDER: 'LOGISTICS_LICENSE',
|
||||
SHIPPING_AGENT: 'LOGISTICS_LICENSE',
|
||||
COMBINED_SA_FF: 'LOGISTICS_LICENSE',
|
||||
MULTIMODAL_TRANSPORT_OPERATOR: 'LOGISTICS_LICENSE',
|
||||
JOINT_INVESTOR: 'LOGISTICS_LICENSE',
|
||||
};
|
||||
|
||||
/**
|
||||
* `FamilyKind` for a type key, for data that predates `familyKind` being a
|
||||
* real column on the server (cached responses from before the rollout, or
|
||||
* anything that only ever had a bare key to go on). Every fresh response now
|
||||
* carries `familyKind` directly from the server's own `family_kind` column —
|
||||
* prefer that over calling this. Kept only as a fallback, and only for keys
|
||||
* this map happens to know about; falls back further to `LOGISTICS_LICENSE`
|
||||
* for anything else, reproducing today's "Licence ___" wording — the safe
|
||||
* default for an unrecognised key.
|
||||
*/
|
||||
export function resolveFamilyKind(licenseTypeKey: string | undefined | null): FamilyKind {
|
||||
return (licenseTypeKey && FAMILY_KIND_BY_KEY[licenseTypeKey]) || 'LOGISTICS_LICENSE';
|
||||
}
|
||||
|
||||
export interface FamilyLabels {
|
||||
/** e.g. "Certificate", "Document", "Licence". */
|
||||
typeLabel: string;
|
||||
/** e.g. "Certificate Number", "Document Number", "Licence Number". */
|
||||
numberLabel: string;
|
||||
/** e.g. "Certificate Holder", "Document Holder", "Licence Holder". */
|
||||
holderLabel: string;
|
||||
/** e.g. "Certificate Review", "Document Review", "Licence Review". */
|
||||
reviewLabel: string;
|
||||
}
|
||||
|
||||
const FAMILY_LABELS: Record<FamilyKind, FamilyLabels> = {
|
||||
CERTIFICATE: {
|
||||
typeLabel: 'Certificate',
|
||||
numberLabel: 'Certificate Number',
|
||||
holderLabel: 'Certificate Holder',
|
||||
reviewLabel: 'Certificate Review',
|
||||
},
|
||||
DOCUMENT: {
|
||||
typeLabel: 'Document',
|
||||
numberLabel: 'Document Number',
|
||||
holderLabel: 'Document Holder',
|
||||
reviewLabel: 'Document Review',
|
||||
},
|
||||
LOGISTICS_LICENSE: {
|
||||
typeLabel: 'Licence',
|
||||
numberLabel: 'Licence Number',
|
||||
holderLabel: 'Licence Holder',
|
||||
reviewLabel: 'Licence Review',
|
||||
},
|
||||
};
|
||||
|
||||
export function familyLabels(familyKind: FamilyKind): FamilyLabels {
|
||||
return FAMILY_LABELS[familyKind];
|
||||
}
|
||||
|
||||
/**
|
||||
* Company name, or applicant name for applications with no company.
|
||||
*
|
||||
* Branches on `familyKind` — the real data-model column — rather than a
|
||||
* hand-maintained key list. A certificate or document is filed by a person,
|
||||
* never a business, so it never has a `companyName` to show; a logistics
|
||||
* licence always does. This is what used to be `APPLICANT_NAME_TYPE_KEYS`, a
|
||||
* frontend array that had to be remembered and kept in sync by hand every
|
||||
* time a new certificate/document type was added — it silently missed
|
||||
* `SEAMAN_BOOK`/`BTC_BASIC_TRAINING`, which is why those rows rendered "—"
|
||||
* instead of the applicant's name in the backoffice queue. `familyKind` is
|
||||
* correct for every current and future type without a matching array update.
|
||||
*/
|
||||
export function applicantOrCompanyName(app: LicenseApplication): string | undefined {
|
||||
if (!app.licenseType?.key || !APPLICANT_NAME_TYPE_KEYS.includes(app.licenseType.key)) {
|
||||
if (app.familyKind === 'LOGISTICS_LICENSE') {
|
||||
return app.companyName ?? undefined;
|
||||
}
|
||||
const applicantName = (app.formData?.account as Record<string, unknown> | undefined)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// Reused rather than redeclared: the department vocabulary belongs to the
|
||||
// seafarer domain, and two copies would drift.
|
||||
import type { SeafarerDepartment } from '../seafarer/seafarer.types';
|
||||
import type { SeafarerDepartment } from "../seafarer/seafarer.types";
|
||||
|
||||
/**
|
||||
* A backend `LocaleValidationDto`. Named for the two locales the UI offers, but
|
||||
@@ -35,6 +35,10 @@ export type LicenseStatus =
|
||||
| "PAYMENT_PENDING"
|
||||
| "PAID"
|
||||
| "PAYMENT_CONFIRMED"
|
||||
// Payment confirmed and pickup date set — for a document printed once and
|
||||
// handed over in person (seaman book, BTC). Most license types skip this
|
||||
// and go straight from PAYMENT_CONFIRMED to CERTIFICATE_ISSUED.
|
||||
| "SCHEDULED"
|
||||
| "CERTIFICATE_ISSUED"
|
||||
| "COMPLETED"
|
||||
// Examined certificates (CoC, some CoP): approval establishes eligibility,
|
||||
@@ -98,12 +102,25 @@ export interface FormSectionConfig {
|
||||
|
||||
/** Grouping the portal organises the licence catalogue by. */
|
||||
export type LicenseCategory =
|
||||
| 'CARGO_FREIGHT'
|
||||
| 'SHIPPING_AGENCY'
|
||||
| 'INVESTMENT'
|
||||
| 'MARITIME_PERSONNEL'
|
||||
| 'VESSEL_SERVICES'
|
||||
| 'WAIVER_SERVICES';
|
||||
| "CARGO_FREIGHT"
|
||||
| "SHIPPING_AGENCY"
|
||||
| "INVESTMENT"
|
||||
| "MARITIME_PERSONNEL"
|
||||
| "VESSEL_SERVICES"
|
||||
| "WAIVER_SERVICES";
|
||||
|
||||
/**
|
||||
* Which business concept a licence type actually is — the real data-model
|
||||
* classification (emaapi's `license_types.family_kind` column), not a label
|
||||
* computed from `key`. A permission granted to a logistics operator, a
|
||||
* seafarer's proof of competence, and a seafarer/vessel's identity or
|
||||
* statutory record are three different things to the people using this
|
||||
* system, even though they run through the identical application pipeline —
|
||||
* see BR-MTO-020. Drives terminology, navigation, and which columns a grid
|
||||
* shows (Company/TIN only make sense for LOGISTICS_LICENSE rows) — never a
|
||||
* workflow or eligibility branch.
|
||||
*/
|
||||
export type FamilyKind = "LOGISTICS_LICENSE" | "CERTIFICATE" | "DOCUMENT";
|
||||
|
||||
export interface LicenseCategoryDefinition {
|
||||
key: LicenseCategory;
|
||||
@@ -131,6 +148,8 @@ export interface LicenseType {
|
||||
name: Bilingual;
|
||||
description?: Bilingual;
|
||||
category: LicenseCategory;
|
||||
/** The real data-model classification — see `FamilyKind`. */
|
||||
familyKind: FamilyKind;
|
||||
certificatePrefix: string;
|
||||
feeNewApplication: string | number | null;
|
||||
feeRenewal: string | number | null;
|
||||
@@ -145,6 +164,8 @@ export interface LicenseType {
|
||||
inspectionRequired: boolean;
|
||||
issuesCertificate: boolean;
|
||||
renewalEnabled: boolean;
|
||||
/** Payment confirmation waits for a scheduled pickup date before issuance. */
|
||||
requiresIssuanceScheduling: boolean;
|
||||
/**
|
||||
* False for person-centric registrations (seafarer): they are open to any
|
||||
* authenticated applicant, live outside the operator catalogue, and have
|
||||
@@ -186,11 +207,7 @@ export interface LicenseType {
|
||||
|
||||
/** What kind of document a certificate type produces. */
|
||||
export type CertificateCategory =
|
||||
| "COC"
|
||||
| "COP"
|
||||
| "ENDORSEMENT"
|
||||
| "GOC"
|
||||
| "NATIONAL";
|
||||
"COC" | "COP" | "ENDORSEMENT" | "GOC" | "NATIONAL";
|
||||
|
||||
/**
|
||||
* STCW responsibility level. Cadet is absent by design — under STCW a cadet is
|
||||
@@ -260,7 +277,10 @@ export interface LicenseApplication {
|
||||
applicationNumber: string;
|
||||
licenseTypeId: string;
|
||||
licenseType?: LicenseType;
|
||||
/** Denormalized from `licenseType.familyKind` at submission time. */
|
||||
familyKind: FamilyKind;
|
||||
applicantUserId: string;
|
||||
parentApplicationId?: string | null;
|
||||
kind: ApplicationKind;
|
||||
status: LicenseStatus;
|
||||
assignedOfficerId: string | null;
|
||||
@@ -279,6 +299,9 @@ export interface LicenseApplication {
|
||||
feeAmount: string | null;
|
||||
feeCurrency: string;
|
||||
issuedLicenseId: string | null;
|
||||
/** Set once an officer schedules pickup for a document requiring in-person handover. */
|
||||
scheduledIssuanceDate: string | null;
|
||||
scheduledBy: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@@ -369,6 +392,7 @@ export interface ApplicationApplicant {
|
||||
|
||||
export interface ApplicationDetail {
|
||||
application: LicenseApplication;
|
||||
relatedApplications?: LicenseApplication[];
|
||||
/** Null when the applicant has no profile row (never expected in practice). */
|
||||
applicant: ApplicationApplicant | null;
|
||||
staff: ApplicationStaff[];
|
||||
@@ -488,11 +512,7 @@ export interface TemplatePageOptions {
|
||||
|
||||
/** Corner the institute logo is anchored to. */
|
||||
export type TemplateLogoCorner =
|
||||
| 'TOP_LEFT'
|
||||
| 'TOP_CENTER'
|
||||
| 'TOP_RIGHT'
|
||||
| 'BOTTOM_LEFT'
|
||||
| 'BOTTOM_RIGHT';
|
||||
"TOP_LEFT" | "TOP_CENTER" | "TOP_RIGHT" | "BOTTOM_LEFT" | "BOTTOM_RIGHT";
|
||||
|
||||
/** Where the institute logo sits on the certificate. */
|
||||
export interface TemplateLogoPlacement {
|
||||
@@ -519,8 +539,8 @@ export interface TemplateFieldPlacement {
|
||||
yPct: number;
|
||||
widthPct: number;
|
||||
fontSize?: number;
|
||||
fontWeight?: 'normal' | 'bold';
|
||||
align?: 'left' | 'center' | 'right';
|
||||
fontWeight?: "normal" | "bold";
|
||||
align?: "left" | "center" | "right";
|
||||
color?: string;
|
||||
}
|
||||
|
||||
@@ -616,6 +636,8 @@ export interface IssuedLicense {
|
||||
certificateNumber: string;
|
||||
licenseTypeId: string;
|
||||
licenseType?: LicenseType;
|
||||
/** Denormalized from `licenseType.familyKind` at issuance time. */
|
||||
familyKind: FamilyKind;
|
||||
applicationId: string;
|
||||
companyName: string | null;
|
||||
tinNumber: string | null;
|
||||
|
||||
@@ -31,6 +31,8 @@ export const LICENSE_PERMISSIONS = {
|
||||
VIEW_INSPECTIONS: "can:View:inspections",
|
||||
CONFIRM_PAYMENT: "can:confirm:license-payment",
|
||||
VIEW_PAYMENTS: "can:View:license-payments",
|
||||
SCHEDULE_ISSUANCE: "can:schedule:license-issuance",
|
||||
ISSUE_CERTIFICATE: "can:issue:license-certificate",
|
||||
CREATE_LICENSE_TYPE: "can:create:license-type",
|
||||
VIEW_LICENSE_TYPES: "can:View:license-types",
|
||||
UPDATE_LICENSE_TYPE: "can:update:license-type",
|
||||
|
||||
Reference in New Issue
Block a user