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

This commit is contained in:
Nati
2026-08-20 07:17:12 +00:00
132 changed files with 9755 additions and 2847 deletions

View File

@@ -398,8 +398,11 @@ export const licensingApi = baseApi
* claim or a decision refreshes the badges along with the list and the
* numbers can never drift from what the grid is showing.
*/
getQueueCounts: builder.query<QueueCounts, void>({
query: () => ({ url: '/license-application-review/counts' }),
getQueueCounts: builder.query<QueueCounts, string | void>({
query: (licenseTypeKey) => ({
url: '/license-application-review/counts',
params: licenseTypeKey ? { licenseTypeKey } : undefined,
}),
providesTags: () => [listTag('ApplicationQueue')],
}),

View File

@@ -173,6 +173,11 @@ export interface LicenseType {
*/
requiresOperatorMode: boolean;
formSchema: { sections: FormSectionConfig[] };
/**
* Which course an application of this type runs. REGISTRATION skips the
* evaluation and inspection stages, so those statuses are unreachable for it.
*/
workflowProfile?: WorkflowProfile;
isActive: boolean;
/** Display order set by EMA; lower comes first. */
sortOrder: number;
@@ -446,6 +451,9 @@ export type QueueSortField =
| "dueAt"
| "claimedAt";
/** Review → evaluation → (inspection) → approval, or the short registration course. */
export type WorkflowProfile = "STANDARD" | "REGISTRATION";
/** Row counts behind the queue's saved-view tabs. */
export interface QueueCounts {
unassigned: number;

View File

@@ -3,6 +3,7 @@ import type {
CreateMedicalCertificate,
CreateSeaServiceRecord,
MedicalCertificate,
RecordQueueFilter,
SeaServiceRecord,
SeaTimeSummary,
SeafarerStatus,
@@ -104,13 +105,25 @@ export const seafarerApi = baseApi
}),
// -------------------------------------------------------- verification
getPendingSeaService: builder.query<SeaServiceRecord[], void>({
query: () => ({ url: '/sea-service-records/pending' }),
getPendingSeaService: builder.query<
SeaServiceRecord[],
RecordQueueFilter | void
>({
query: (status) => ({
url: '/sea-service-records/pending',
params: { status: status || 'SUBMITTED' },
}),
providesTags: () => [listTag('SeaServiceRecord')],
}),
getPendingMedical: builder.query<MedicalCertificate[], void>({
query: () => ({ url: '/medical-certificates/pending' }),
getPendingMedical: builder.query<
MedicalCertificate[],
RecordQueueFilter | void
>({
query: (status) => ({
url: '/medical-certificates/pending',
params: { status: status || 'SUBMITTED' },
}),
providesTags: () => [listTag('MedicalCertificate')],
}),

View File

@@ -1,4 +1,7 @@
export type SeafarerRecordStatus = 'SUBMITTED' | 'VERIFIED' | 'REJECTED';
/** Verification-queue filter: a record status, or ALL for no filter. */
export type RecordQueueFilter = SeafarerRecordStatus | 'ALL';
export type MedicalFitness = 'FIT' | 'FIT_WITH_RESTRICTIONS' | 'UNFIT';
export type SeafarerDepartment = 'DECK' | 'ENGINE' | 'CATERING';
export type SeafarerStatus = 'ACTIVE' | 'INACTIVE' | 'PENDING' | 'SUSPENDED';

View File

@@ -3,6 +3,8 @@ import type {
CreateVesselIncident,
Vessel,
VesselIncident,
VesselReport,
VesselReportQuery,
VesselStatus,
} from './vessel.types';
@@ -35,6 +37,22 @@ export const vesselApi = baseApi
providesTags: () => [listTag('Vessel')],
}),
/**
* The whole backoffice dashboard in one call — KPIs, time series,
* breakdowns and worklists. Backoffice only (`can:View:vessel-registry`).
*
* Array filters are passed as arrays, not joined strings: the API accepts
* both the repeated and the comma-separated form, and `params` serialises
* the repeated one.
*/
getVesselReport: builder.query<VesselReport, VesselReportQuery | void>({
query: (params) => ({
url: '/vessels/report',
params: params ?? undefined,
}),
providesTags: () => [listTag('Vessel')],
}),
getVessel: builder.query<Vessel, string>({
query: (id) => ({ url: `/vessels/${id}` }),
providesTags: (_r, _e, id) => [{ type: 'Vessel', id }],
@@ -77,6 +95,7 @@ export const vesselApi = baseApi
export const {
useGetMyVesselsQuery,
useGetVesselsQuery,
useGetVesselReportQuery,
useGetVesselQuery,
useUpdateVesselStatusMutation,
useGetVesselIncidentsQuery,

View File

@@ -49,3 +49,247 @@ export interface CreateVesselIncident {
description: string;
severity?: string;
}
// ---------------------------------------------------------------------------
// Vessel registration report (GET /vessels/report)
//
// One call fills the whole backoffice dashboard. Unlike `Vessel` above, every
// numeric field here is already a real number — the API casts the Postgres
// `numeric` strings before it answers.
export type ReportGranularity = 'DAY' | 'WEEK' | 'MONTH';
/**
* One slice of a breakdown chart.
*
* `percentage` is of the whole, not of the slices that survived the `topN`
* cut, so a set of slices always totals 100.
*/
export interface BreakdownItem {
key: string;
label: string;
count: number;
percentage: number;
}
export interface VesselReportQuery {
/** Bounds the time series and the "in period" figures only. */
from?: string;
to?: string;
granularity?: ReportGranularity;
category?: VesselCategory[];
status?: VesselStatus[];
flagState?: string[];
portOfRegistry?: string[];
vesselType?: string[];
search?: string;
expiringWithinDays?: number;
/** Slices kept per high-cardinality chart; the tail collapses into "Other". */
topN?: number;
tableLimit?: number;
}
export interface RegisterKpis {
total: number;
registered: number;
suspended: number;
deregistered: number;
registeredInPeriod: number;
registeredInPreviousPeriod: number;
/** Null when there is no previous period to compare against. */
changePct: number | null;
}
export interface FleetKpis {
totalGrossTonnage: number;
avgGrossTonnage: number | null;
/** How many hulls the tonnage average actually covers. */
grossTonnageKnownFor: number;
totalPassengerCapacity: number;
avgLengthMeters: number | null;
avgAgeYears: number | null;
ageKnownFor: number;
seaGoing: number;
inlandWaterway: number;
}
export interface PipelineKpis {
total: number;
draft: number;
inProgress: number;
approved: number;
rejected: number;
issued: number;
submittedInPeriod: number;
decidedInPeriod: number;
newCount: number;
renewalCount: number;
/** Approved over settled. Null while nothing has been decided. */
approvalRatePct: number | null;
avgProcessingDays: number | null;
medianProcessingDays: number | null;
avgAdjustmentRounds: number | null;
}
export interface CertificateKpis {
total: number;
active: number;
expired: number;
suspended: number;
/** Cumulative: a certificate due in 11 days is inside all three. */
expiringIn30: number;
expiringIn60: number;
expiringIn90: number;
missingCertificate: number;
}
export interface IncidentKpis {
total: number;
inPeriod: number;
reportedByOfficer: number;
reportedByOwner: number;
vesselsWithIncidents: number;
}
export interface RevenueKpis {
currency: string;
/** True when more than one currency was summed — warn rather than total. */
mixedCurrency: boolean;
paid: number;
pending: number;
paidCount: number;
pendingCount: number;
failedCount: number;
}
/** Bucketed series. `bucket` is an ISO date; the window is zero-filled. */
export interface RegistrationBucket {
bucket: string;
count: number;
grossTonnage: number;
}
export interface ApplicationBucket {
bucket: string;
submitted: number;
approved: number;
rejected: number;
issued: number;
}
export interface IncidentBucket {
bucket: string;
count: number;
}
export interface RevenueBucket {
bucket: string;
amount: number;
count: number;
}
export interface ExpiringCertificateRow {
vesselId: string;
registrationNumber: string;
name: string;
ownerName: string | null;
ownerUserId: string;
certificateNumber: string | null;
expiryDate: string;
certificateStatus: string | null;
/** 0 means it expires today, which still counts as live. */
daysToExpiry: number;
}
export interface RecentRegistrationRow {
vesselId: string;
registrationNumber: string;
name: string;
category: VesselCategory;
vesselType: string | null;
flagState: string | null;
grossTonnage: number | null;
ownerName: string | null;
status: VesselStatus;
registeredAt: string;
}
export interface RecentIncidentRow {
id: string;
vesselId: string;
registrationNumber: string;
vesselName: string;
occurredAt: string;
severity: string | null;
location: string | null;
description: string;
reportedByOfficer: boolean;
}
export interface PendingApplicationRow {
applicationNumber: string;
status: string;
kind: 'NEW' | 'RENEWAL';
assignedOfficerId: string | null;
submittedAt: string | null;
adjustmentRound: number;
daysOpen: number;
}
export interface VesselReport {
generatedAt: string;
/** True when the register passed the API's scan cap — figures are partial. */
truncated: boolean;
filters: {
from: string;
to: string;
granularity: ReportGranularity;
expiringWithinDays: number;
topN: number;
tableLimit: number;
category: VesselCategory[] | null;
status: VesselStatus[] | null;
flagState: string[] | null;
portOfRegistry: string[] | null;
vesselType: string[] | null;
search: string | null;
};
kpis: {
register: RegisterKpis;
fleet: FleetKpis;
pipeline: PipelineKpis;
certificates: CertificateKpis;
incidents: IncidentKpis;
revenue: RevenueKpis;
};
timeSeries: {
registrations: RegistrationBucket[];
applications: ApplicationBucket[];
incidents: IncidentBucket[];
revenue: RevenueBucket[];
};
breakdowns: {
byStatus: BreakdownItem[];
byCategory: BreakdownItem[];
byFlagState: BreakdownItem[];
byPortOfRegistry: BreakdownItem[];
byVesselType: BreakdownItem[];
byHullMaterial: BreakdownItem[];
byEngineType: BreakdownItem[];
byTonnageBand: BreakdownItem[];
byLengthBand: BreakdownItem[];
byAgeBand: BreakdownItem[];
byBuildDecade: BreakdownItem[];
byApplicationStatus: BreakdownItem[];
byApplicationKind: BreakdownItem[];
/** `key` is an IAM user uuid, or the literal "UNASSIGNED". */
byOfficer: BreakdownItem[];
byIncidentSeverity: BreakdownItem[];
};
tables: {
expiringCertificates: ExpiringCertificateRow[];
recentRegistrations: RecentRegistrationRow[];
recentIncidents: RecentIncidentRow[];
pendingApplications: PendingApplicationRow[];
};
}