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

This commit is contained in:
Estifo77
2026-08-19 10:23:33 +03:00
74 changed files with 7969 additions and 3617 deletions

View File

@@ -2,6 +2,7 @@ export * from './lib/base-api';
export * from './lib/query-and-mutation';
export * from './lib/session';
export * from './lib/features/licensing';
export * from './lib/features/location';
export * from './lib/features/seafarer';
export * from './lib/features/vessel';
export { baseQueryWithReauth, configureTokenRefresh } from './lib/base-api/base-query-with-reauth';

View File

@@ -1,6 +1,7 @@
import { resolveTokenFromStorage } from '../../session';
import type {
Bilingual,
FormFieldConfig,
FormSectionConfig,
LicenseApplication,
LicenseStatus,
@@ -178,12 +179,64 @@ export function applicantOrCompanyName(app: LicenseApplication): string | undefi
return typeof applicantName === 'string' && applicantName ? applicantName : undefined;
}
/**
* One form answer as a person should read it back.
*
* The stored value is not it: a SELECT holds the option's `value`, so an
* unformatted view shows reviewers and applicants `AB_POSITIVE` and `DECK` —
* the codes the database wants, not the words that were chosen. Shared by the
* applicant's summary and the officer's review so the two never describe the
* same application differently.
*
* `showDate` is passed in rather than imported: date display is a hook
* (`useDateDisplayer`, Ethiopian-calendar aware) and this is a plain function.
*/
export function displayFieldValue(
field: Pick<FormFieldConfig, 'type' | 'options'>,
raw: unknown,
opts: {
language?: string;
showDate?: (value: string) => string;
currency?: string;
} = {},
): string {
if (raw === null || raw === undefined || raw === '') return '';
const { language = 'en', showDate, currency } = opts;
switch (field.type) {
case 'BOOLEAN':
return raw ? 'Yes' : 'No';
case 'DATE':
return showDate?.(String(raw)) || String(raw);
case 'SELECT': {
const option = field.options?.find((o) => o.value === raw);
// Falls back to the stored value rather than blanking: an option removed
// from the config since this was filed still has to show what was chosen.
return option ? localized(option.label, language) : String(raw);
}
case 'MONEY': {
const amount = Number(raw);
return Number.isFinite(amount)
? `${amount.toLocaleString()} ${currency ?? ''}`.trim()
: String(raw);
}
default:
return String(raw);
}
}
/** Reads a bilingual value for the active language, falling back to English. */
export function localized(value: Bilingual | undefined, language = 'en'): string {
if (!value) return '';
// `||` not `??`: an empty Amharic string is "not translated", not a value —
// editors save `am: ''` freely (ExamPage, CertificationPage, LocationForm).
return (language === 'am' ? value.am : value.en) || value.en || value.am || '';
//
// Keyed by the active language rather than an en/am ternary, so a locale the
// backend stores but the UI does not yet offer a switcher for (`om`, `so` on
// location names) still resolves once it does. English then Amharic remain
// the fallbacks, in that order.
const active = value[language as keyof Bilingual];
return active || value.en || value.am || '';
}
/**

View File

@@ -4,7 +4,18 @@
// seafarer domain, and two copies would drift.
import type { SeafarerDepartment } from '../seafarer/seafarer.types';
export type Bilingual = { en?: string; am?: string };
/**
* A backend `LocaleValidationDto`. Named for the two locales the UI offers, but
* carries every locale the column stores — location names are seeded with `om`
* (Finfinnee) and `so` too, and a type that declared only en/am made those
* unreachable through the typed client.
*/
export type Bilingual = {
en?: string;
am?: string;
om?: string;
so?: string;
};
/**
* The application status vocabulary. Single source of truth for both apps —
@@ -329,8 +340,37 @@ export interface ApplicationRemark {
createdAt: string;
}
/**
* Who filed the application, from their profile.
*
* Served alongside the form because a person-centric service (seafarer
* registration, a certificate) has no `companyName` to identify itself by — a
* reviewer opening one otherwise sees only an application number and has to
* infer the human from the answers.
*/
export interface ApplicationApplicant {
profileId: string;
firstName: string | null;
middleName: string | null;
lastName: string | null;
gender: string | null;
dob: string | null;
pob: string | null;
maritalStatus: string | null;
seafarerNumber: string | null;
seafarerStatus: string | null;
seafarerDepartment: string | null;
nationality: string | null;
idType: string | null;
idNumber: string | null;
primaryPhoneNumber: string | null;
email: string | null;
}
export interface ApplicationDetail {
application: LicenseApplication;
/** Null when the applicant has no profile row (never expected in practice). */
applicant: ApplicationApplicant | null;
staff: ApplicationStaff[];
attachments: Attachment[];
history: StatusHistoryEntry[];

View File

@@ -0,0 +1 @@
export * from './location.types';

View File

@@ -0,0 +1,47 @@
/** Shared location contract — mirrors the `iam.locations` tree in emaapi. */
import type { Bilingual } from '../licensing/licensing.types';
/**
* One node of the location tree.
*
* Both apps read the same `/locations` route, so the model lives here rather
* than in each app's own feature folder — the two hand-maintained copies had
* already drifted (the portal's lacked `locationType`, `children` and the
* timestamps, and its query accepted no `parentId` filter even though the
* backend supports one).
*
* `names` is `Bilingual`, the backend's `LocaleValidationDto`: seeded location
* names carry `om` and `so` alongside `en`/`am`, which the previous
* `{ en, am }` pair made unreachable.
*/
export interface Location {
id: string;
code: string;
names: Bilingual;
locationTypeId: string;
parentId: string | null;
locationType?: LocationType;
children?: Location[];
createdAt?: string;
updatedAt?: string;
}
/**
* A level in the tree. `level` orders them (City 1 → Sub-city 2 → Woreda 3 →
* Kebele 4); `code` is what both sides key behaviour off, so it is the field to
* match on rather than the display name.
*/
export interface LocationType {
id: string;
code: string;
names: Bilingual;
level: number;
createdAt?: string;
updatedAt?: string;
}
export interface ListResponse<T> {
count: number;
items: T[];
}