mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
change
This commit is contained in:
@@ -2,7 +2,6 @@ import type { Dispatch, ReactNode, SetStateAction } from "react";
|
||||
import { Badge, Checkbox, Text, Tooltip } from "@mantine/core";
|
||||
import type { TFunction } from "i18next";
|
||||
import {
|
||||
APPLICANT_NAME_TYPE_KEYS,
|
||||
STATUS_COLORS,
|
||||
STATUS_LABELS,
|
||||
applicantOrCompanyName,
|
||||
@@ -14,11 +13,28 @@ import type { AdvancedColumn } from "@ema-platform/ui";
|
||||
import { dateDisplayer } from "@ema-platform/shared";
|
||||
import { computeSla } from "../../sla";
|
||||
|
||||
/**
|
||||
* Label for the Company/Applicant column, derived from the rows actually on
|
||||
* screen rather than the route — a type-pinned queue (`/type/:typeCode`)
|
||||
* happens to be one family, but nothing stops the mixed "All Applications"
|
||||
* grid from holding both, and a static header can't be correct for both at
|
||||
* once. Falls back to the combined label until the page has data to look at.
|
||||
*/
|
||||
function companyColumnHeader(t: TFunction, items: LicenseApplication[]): string {
|
||||
if (items.length === 0) {
|
||||
return t("queue.companyOrApplicant", "Applicant / Company");
|
||||
}
|
||||
const allLogistics = items.every((a) => a.familyKind === "LOGISTICS_LICENSE");
|
||||
const allNonLogistics = items.every((a) => a.familyKind !== "LOGISTICS_LICENSE");
|
||||
if (allLogistics) return t("queue.company", "Company");
|
||||
if (allNonLogistics) return t("queue.applicant", "Applicant");
|
||||
return t("queue.companyOrApplicant", "Applicant / Company");
|
||||
}
|
||||
|
||||
export function licenseQueueColumns(
|
||||
t: TFunction,
|
||||
locale: string,
|
||||
opts: {
|
||||
typeCode: string | undefined;
|
||||
items: LicenseApplication[];
|
||||
selected: string[];
|
||||
setSelected: Dispatch<SetStateAction<string[]>>;
|
||||
@@ -29,8 +45,7 @@ export function licenseQueueColumns(
|
||||
) => ReactNode;
|
||||
},
|
||||
): AdvancedColumn<LicenseApplication>[] {
|
||||
const { typeCode, items, selected, setSelected, allSelected, sortableHeader } =
|
||||
opts;
|
||||
const { items, selected, setSelected, allSelected, sortableHeader } = opts;
|
||||
return [
|
||||
{
|
||||
header: (
|
||||
@@ -72,12 +87,13 @@ export function licenseQueueColumns(
|
||||
),
|
||||
},
|
||||
{
|
||||
header: sortableHeader(
|
||||
typeCode && APPLICANT_NAME_TYPE_KEYS.includes(typeCode)
|
||||
? t("queue.applicant", "Applicant")
|
||||
: t("queue.company", "Company"),
|
||||
"companyName",
|
||||
),
|
||||
// Header reflects what's actually on screen, not the route: a
|
||||
// type-pinned queue (`typeCode` set) is always one family, but the
|
||||
// mixed "All Applications" grid can hold logistics rows and
|
||||
// certificate/document rows side by side, so no single static label is
|
||||
// right for the whole column there — "Applicant / Company" covers
|
||||
// both without claiming a row is one or the other.
|
||||
header: sortableHeader(companyColumnHeader(t, items), "companyName"),
|
||||
label: t("queue.company", "Company"),
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm">{applicantOrCompanyName(row.original) ?? "—"}</Text>
|
||||
@@ -85,11 +101,17 @@ export function licenseQueueColumns(
|
||||
},
|
||||
{
|
||||
header: t("queue.tin", "TIN"),
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm" c="dimmed">
|
||||
{row.original.tinNumber ?? "—"}
|
||||
</Text>
|
||||
),
|
||||
cell: ({ row }) =>
|
||||
// A certificate/document application is filed by a person, never a
|
||||
// business — there is no TIN to show, not even a blank one. Hiding
|
||||
// the cell (rather than "—") is what makes the row visibly not a
|
||||
// logistics-licence row, which is the whole point of `familyKind`
|
||||
// being a real column now instead of a frontend guess.
|
||||
row.original.familyKind === "LOGISTICS_LICENSE" ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
{row.original.tinNumber ?? "—"}
|
||||
</Text>
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
header: t("queue.typeCol", "Type"),
|
||||
|
||||
@@ -375,7 +375,6 @@ export function LicenseQueuePage() {
|
||||
const columns: AdvancedColumn<LicenseApplication>[] = useMemo(
|
||||
() => [
|
||||
...licenseQueueColumns(t, i18n.language, {
|
||||
typeCode,
|
||||
items,
|
||||
selected,
|
||||
setSelected,
|
||||
@@ -397,7 +396,6 @@ export function LicenseQueuePage() {
|
||||
allSelected,
|
||||
items,
|
||||
claiming,
|
||||
typeCode,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -823,6 +823,7 @@ export const am: Translations = {
|
||||
number: "ማመልከቻ ቁ.",
|
||||
company: "ኩባንያ",
|
||||
applicant: "አመልካች",
|
||||
companyOrApplicant: "አመልካች / ኩባንያ",
|
||||
tin: "ቲን",
|
||||
submitted: "የቀረበበት",
|
||||
sla: "ዕድሜ / የጊዜ ገደብ",
|
||||
|
||||
@@ -827,6 +827,10 @@ export const en = {
|
||||
number: 'App #',
|
||||
company: 'Company',
|
||||
applicant: 'Applicant',
|
||||
// Column header when the grid holds both logistics-licence rows (which
|
||||
// have a company) and certificate/document rows (which have an
|
||||
// applicant instead) — the mixed "All Applications" queue.
|
||||
companyOrApplicant: 'Applicant / Company',
|
||||
tin: 'TIN',
|
||||
submitted: 'Submitted',
|
||||
sla: 'Age / SLA',
|
||||
|
||||
@@ -79,10 +79,15 @@ export function LicenseCatalogue() {
|
||||
const { groups, orphans } = useMemo(() => {
|
||||
const active = (types?.items ?? [])
|
||||
.filter((t) => t.isActive)
|
||||
// Person-centric registrations (seafarer) are not operator licences:
|
||||
// they can never be declared as a mode, have their own entry points,
|
||||
// and would only confuse this catalogue — even under "show all".
|
||||
.filter((t) => t.requiresOperatorMode !== false)
|
||||
// Logistics licences only: this is the operator catalogue, not the
|
||||
// seafarer certificate or vessel/seafarer document catalogue — those
|
||||
// have their own entry points. `familyKind` is the real data-model
|
||||
// classification (set on the type at seed time); `requiresOperatorMode`
|
||||
// was the proxy this used before that column existed and happened to
|
||||
// agree for every type seeded so far, but a type can only be trusted to
|
||||
// stay in sync with the catalogue it belongs in if the catalogue reads
|
||||
// its actual family instead of a flag with a different purpose.
|
||||
.filter((t) => t.familyKind === 'LOGISTICS_LICENSE')
|
||||
// Only what the applicant operates as. The server enforces the same rule
|
||||
// on create; this is what stops them starting an application they will
|
||||
// be refused at the end of.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolveTokenFromStorage } from '../../session';
|
||||
import type {
|
||||
Bilingual,
|
||||
FamilyKind,
|
||||
FormFieldConfig,
|
||||
FormSectionConfig,
|
||||
LicenseApplication,
|
||||
@@ -172,21 +173,6 @@ export const APPLICANT_NAME_TYPE_KEYS = [
|
||||
'ENDORSEMENT_GOC',
|
||||
];
|
||||
|
||||
/**
|
||||
* Which business concept a licence type actually is, for terminology and
|
||||
* navigation only — never a workflow or eligibility branch. Mirrors
|
||||
* `resolveFamilyKind` in emaapi's `common/utils/family-terminology.ts`; the
|
||||
* two must be kept in step by hand since the value isn't sent over the wire
|
||||
* (nothing needs it server-side beyond notification copy, so a shared
|
||||
* package felt like more plumbing than the duplication it would save).
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export type FamilyKind = 'LOGISTICS_LICENSE' | 'CERTIFICATE' | 'DOCUMENT';
|
||||
|
||||
const FAMILY_KIND_BY_KEY: Partial<Record<string, FamilyKind>> = {
|
||||
SEAMAN_BOOK: 'DOCUMENT',
|
||||
VESSEL_REGISTRATION: 'DOCUMENT',
|
||||
@@ -203,10 +189,14 @@ const FAMILY_KIND_BY_KEY: Partial<Record<string, FamilyKind>> = {
|
||||
};
|
||||
|
||||
/**
|
||||
* `FamilyKind` for a type key that may not be in the map yet. Falls back to
|
||||
* `LOGISTICS_LICENSE`, reproducing today's "Licence ___" wording — the safe
|
||||
* default for an unrecognised key, matching how the map above treats config
|
||||
* it doesn't know about.
|
||||
* `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';
|
||||
@@ -248,9 +238,21 @@ export function familyLabels(familyKind: FamilyKind): FamilyLabels {
|
||||
return FAMILY_LABELS[familyKind];
|
||||
}
|
||||
|
||||
/** Company name, or applicant name for licence types that have no company. */
|
||||
/**
|
||||
* 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)
|
||||
|
||||
@@ -109,6 +109,19 @@ export type LicenseCategory =
|
||||
| "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;
|
||||
name: Bilingual;
|
||||
@@ -135,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;
|
||||
@@ -257,6 +272,8 @@ 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;
|
||||
@@ -611,6 +628,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;
|
||||
|
||||
Reference in New Issue
Block a user