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

This commit is contained in:
nati14575
2026-08-19 16:49:12 +03:00
19 changed files with 818 additions and 189 deletions

View File

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

View File

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

View File

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