mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
@@ -19,7 +19,6 @@ import { KpiTiles } from './KpiTiles';
|
||||
import { ReportCharts } from './ReportCharts';
|
||||
import { ReportFilters } from './ReportFilters';
|
||||
import { ReportTables } from './ReportTables';
|
||||
import { MOCK_VESSEL_REPORT } from './mock-report';
|
||||
import { queryToSearchParams, searchParamsToQuery } from './report-format';
|
||||
|
||||
/**
|
||||
@@ -52,11 +51,9 @@ export function VesselRegistrationReportPage() {
|
||||
[setSearchParams],
|
||||
);
|
||||
|
||||
const { data, isLoading, isFetching, error } = useGetVesselReportQuery(query);
|
||||
|
||||
// Dev-only stand-in so the dashboard can be reviewed before the endpoint is
|
||||
// live. Production still shows the empty state and the error alert.
|
||||
const report = data ?? (import.meta.env.DEV ? MOCK_VESSEL_REPORT : undefined);
|
||||
const { data: report, isLoading, isFetching, error } = useGetVesselReportQuery(
|
||||
query,
|
||||
);
|
||||
|
||||
const exportCsv = useCallback(async () => {
|
||||
setExporting(true);
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
import type { BreakdownItem, VesselReport } from '@ema-platform/api';
|
||||
|
||||
/**
|
||||
* A stand-in report for previewing the dashboard before `GET /vessels/report`
|
||||
* is live. Only used in dev, and only when the real call gives nothing back.
|
||||
*
|
||||
* ponytail: hand-written fixture, delete once the endpoint answers.
|
||||
*/
|
||||
|
||||
const MONTHS = [
|
||||
'2025-09-01', '2025-10-01', '2025-11-01', '2025-12-01',
|
||||
'2026-01-01', '2026-02-01', '2026-03-01', '2026-04-01',
|
||||
'2026-05-01', '2026-06-01', '2026-07-01', '2026-08-01',
|
||||
];
|
||||
|
||||
/** Turns `[label, count]` pairs into slices whose percentages total 100. */
|
||||
function breakdown(pairs: [string, number][]): BreakdownItem[] {
|
||||
const total = pairs.reduce((sum, [, count]) => sum + count, 0) || 1;
|
||||
return pairs.map(([label, count]) => ({
|
||||
key: label.toUpperCase().replace(/[^A-Z0-9]+/g, '_'),
|
||||
label,
|
||||
count,
|
||||
percentage: Math.round((count / total) * 1000) / 10,
|
||||
}));
|
||||
}
|
||||
|
||||
const REGISTRATIONS = [18, 24, 21, 30, 27, 33, 29, 38, 35, 41, 37, 26];
|
||||
const SUBMITTED = [26, 31, 28, 39, 34, 42, 37, 47, 44, 51, 46, 33];
|
||||
|
||||
export const MOCK_VESSEL_REPORT: VesselReport = {
|
||||
generatedAt: '2026-08-18T09:15:00.000Z',
|
||||
truncated: false,
|
||||
filters: {
|
||||
from: '2025-09-01',
|
||||
to: '2026-08-18',
|
||||
granularity: 'MONTH',
|
||||
expiringWithinDays: 90,
|
||||
topN: 8,
|
||||
tableLimit: 10,
|
||||
category: null,
|
||||
status: null,
|
||||
flagState: null,
|
||||
portOfRegistry: null,
|
||||
vesselType: null,
|
||||
search: null,
|
||||
},
|
||||
kpis: {
|
||||
register: {
|
||||
total: 1284,
|
||||
registered: 1096,
|
||||
suspended: 87,
|
||||
deregistered: 101,
|
||||
registeredInPeriod: 359,
|
||||
registeredInPreviousPeriod: 302,
|
||||
changePct: 18.9,
|
||||
},
|
||||
fleet: {
|
||||
totalGrossTonnage: 486_320,
|
||||
avgGrossTonnage: 412.7,
|
||||
grossTonnageKnownFor: 1178,
|
||||
totalPassengerCapacity: 39_450,
|
||||
avgLengthMeters: 28.4,
|
||||
avgAgeYears: 16.2,
|
||||
ageKnownFor: 1141,
|
||||
seaGoing: 512,
|
||||
inlandWaterway: 772,
|
||||
},
|
||||
pipeline: {
|
||||
total: 642,
|
||||
draft: 48,
|
||||
inProgress: 113,
|
||||
approved: 351,
|
||||
rejected: 62,
|
||||
issued: 337,
|
||||
submittedInPeriod: 458,
|
||||
decidedInPeriod: 396,
|
||||
newCount: 421,
|
||||
renewalCount: 221,
|
||||
approvalRatePct: 85,
|
||||
avgProcessingDays: 11.4,
|
||||
medianProcessingDays: 9,
|
||||
avgAdjustmentRounds: 1.3,
|
||||
},
|
||||
certificates: {
|
||||
total: 1096,
|
||||
active: 934,
|
||||
expired: 118,
|
||||
suspended: 44,
|
||||
expiringIn30: 37,
|
||||
expiringIn60: 71,
|
||||
expiringIn90: 112,
|
||||
missingCertificate: 23,
|
||||
},
|
||||
incidents: {
|
||||
total: 96,
|
||||
inPeriod: 41,
|
||||
reportedByOfficer: 58,
|
||||
reportedByOwner: 38,
|
||||
vesselsWithIncidents: 74,
|
||||
},
|
||||
revenue: {
|
||||
currency: 'ETB',
|
||||
mixedCurrency: false,
|
||||
paid: 4_812_500,
|
||||
pending: 638_000,
|
||||
paidCount: 389,
|
||||
pendingCount: 54,
|
||||
failedCount: 12,
|
||||
},
|
||||
},
|
||||
timeSeries: {
|
||||
registrations: MONTHS.map((bucket, i) => ({
|
||||
bucket,
|
||||
count: REGISTRATIONS[i],
|
||||
grossTonnage: REGISTRATIONS[i] * 380 + i * 260,
|
||||
})),
|
||||
applications: MONTHS.map((bucket, i) => ({
|
||||
bucket,
|
||||
submitted: SUBMITTED[i],
|
||||
approved: Math.round(SUBMITTED[i] * 0.72),
|
||||
rejected: Math.round(SUBMITTED[i] * 0.11),
|
||||
issued: Math.round(SUBMITTED[i] * 0.68),
|
||||
})),
|
||||
incidents: MONTHS.map((bucket, i) => ({
|
||||
bucket,
|
||||
count: [4, 6, 3, 9, 5, 7, 4, 11, 8, 6, 9, 5][i],
|
||||
})),
|
||||
revenue: MONTHS.map((bucket, i) => ({
|
||||
bucket,
|
||||
amount: 280_000 + REGISTRATIONS[i] * 9_400,
|
||||
count: SUBMITTED[i],
|
||||
})),
|
||||
},
|
||||
breakdowns: {
|
||||
byStatus: breakdown([
|
||||
['Registered', 1096],
|
||||
['Suspended', 87],
|
||||
['Deregistered', 101],
|
||||
]),
|
||||
byCategory: breakdown([
|
||||
['Inland waterway', 772],
|
||||
['Sea-going', 512],
|
||||
]),
|
||||
byFlagState: breakdown([
|
||||
['Ethiopia', 918], ['Djibouti', 142], ['Panama', 88], ['Liberia', 54],
|
||||
['Marshall Islands', 37], ['Malta', 21], ['Other', 24],
|
||||
]),
|
||||
byPortOfRegistry: breakdown([
|
||||
['Bahir Dar', 286], ['Djibouti', 241], ['Gambella', 198],
|
||||
['Hawassa', 176], ['Arba Minch', 154], ['Tana Kirkos', 121],
|
||||
['Other', 108],
|
||||
]),
|
||||
byVesselType: breakdown([
|
||||
['Passenger ferry', 341], ['Cargo', 289], ['Fishing', 214],
|
||||
['Tug', 132], ['Barge', 118], ['Tanker', 76], ['Research', 41],
|
||||
['Other', 73],
|
||||
]),
|
||||
byHullMaterial: breakdown([
|
||||
['Steel', 612], ['Aluminium', 288], ['Fibreglass', 241],
|
||||
['Wood', 108], ['Composite', 35],
|
||||
]),
|
||||
byEngineType: breakdown([
|
||||
['Diesel', 874], ['Outboard petrol', 246], ['Electric', 87],
|
||||
['Hybrid', 44], ['Sail / none', 33],
|
||||
]),
|
||||
byTonnageBand: breakdown([
|
||||
['< 50 GT', 402], ['50-199 GT', 348], ['200-499 GT', 241],
|
||||
['500-999 GT', 152], ['1000+ GT', 141],
|
||||
]),
|
||||
byLengthBand: breakdown([
|
||||
['< 12 m', 318], ['12-24 m', 421], ['24-50 m', 336],
|
||||
['50-100 m', 148], ['100 m+', 61],
|
||||
]),
|
||||
byAgeBand: breakdown([
|
||||
['0-5 yrs', 214], ['6-10 yrs', 287], ['11-20 yrs', 396],
|
||||
['21-30 yrs', 241], ['30+ yrs', 146],
|
||||
]),
|
||||
byBuildDecade: breakdown([
|
||||
['1980s', 96], ['1990s', 187], ['2000s', 341],
|
||||
['2010s', 452], ['2020s', 208],
|
||||
]),
|
||||
byApplicationStatus: breakdown([
|
||||
['Issued', 337], ['Approved', 14], ['Under review', 113],
|
||||
['Draft', 48], ['Rejected', 62], ['Correction required', 68],
|
||||
]),
|
||||
byApplicationKind: breakdown([
|
||||
['New', 421],
|
||||
['Renewal', 221],
|
||||
]),
|
||||
byOfficer: [
|
||||
{ key: '5f1c1b8e-0001-4a11-9c31-0a1b2c3d4e01', label: 'Selam Bekele', count: 148, percentage: 23.1 },
|
||||
{ key: '5f1c1b8e-0002-4a11-9c31-0a1b2c3d4e02', label: 'Dawit Tesfaye', count: 132, percentage: 20.6 },
|
||||
{ key: '5f1c1b8e-0003-4a11-9c31-0a1b2c3d4e03', label: 'Hanna Girma', count: 117, percentage: 18.2 },
|
||||
{ key: '5f1c1b8e-0004-4a11-9c31-0a1b2c3d4e04', label: 'Yonas Alemu', count: 94, percentage: 14.6 },
|
||||
{ key: '5f1c1b8e-0005-4a11-9c31-0a1b2c3d4e05', label: 'Meron Assefa', count: 78, percentage: 12.1 },
|
||||
{ key: 'UNASSIGNED', label: 'Unassigned', count: 73, percentage: 11.4 },
|
||||
],
|
||||
byIncidentSeverity: breakdown([
|
||||
['Minor', 51], ['Moderate', 28], ['Major', 13], ['Critical', 4],
|
||||
]),
|
||||
},
|
||||
tables: {
|
||||
expiringCertificates: [
|
||||
{ registrationNumber: 'ET-VR-004182', name: 'Abay Star', ownerName: 'Blue Nile Transport PLC', certificateNumber: 'CERT-2023-004182', expiryDate: '2026-08-24', daysToExpiry: 6 },
|
||||
{ registrationNumber: 'ET-VR-003907', name: 'Tana Pearl', ownerName: 'Lake Tana Ferries', certificateNumber: 'CERT-2023-003907', expiryDate: '2026-09-02', daysToExpiry: 15 },
|
||||
{ registrationNumber: 'ET-VR-004411', name: 'Gambella Express', ownerName: 'Baro River Logistics', certificateNumber: 'CERT-2023-004411', expiryDate: '2026-09-11', daysToExpiry: 24 },
|
||||
{ registrationNumber: 'ET-VR-003655', name: 'Awash Carrier', ownerName: 'Awash Marine Services', certificateNumber: 'CERT-2023-003655', expiryDate: '2026-09-19', daysToExpiry: 32 },
|
||||
{ registrationNumber: 'ET-VR-004098', name: 'Rift Valley II', ownerName: 'Hawassa Boat Union', certificateNumber: 'CERT-2023-004098', expiryDate: '2026-10-04', daysToExpiry: 47 },
|
||||
{ registrationNumber: 'ET-VR-004320', name: 'Omo Trader', ownerName: 'Omo Cargo Co.', certificateNumber: 'CERT-2023-004320', expiryDate: '2026-10-18', daysToExpiry: 61 },
|
||||
{ registrationNumber: 'ET-VR-003812', name: 'Zeway Runner', ownerName: 'Zeway Fisheries', certificateNumber: 'CERT-2023-003812', expiryDate: '2026-11-01', daysToExpiry: 75 },
|
||||
{ registrationNumber: 'ET-VR-004501', name: 'Djibouti Link', ownerName: 'Horn Shipping Ltd', certificateNumber: 'CERT-2023-004501', expiryDate: '2026-11-13', daysToExpiry: 87 },
|
||||
].map((row, i) => ({
|
||||
...row,
|
||||
vesselId: `9a0e0000-0000-4000-8000-0000000${1000 + i}`,
|
||||
ownerUserId: `7b1f0000-0000-4000-8000-0000000${2000 + i}`,
|
||||
certificateStatus: 'ACTIVE',
|
||||
})),
|
||||
recentRegistrations: ([
|
||||
{ registrationNumber: 'ET-VR-004612', name: 'Blue Horizon', category: 'SEA_GOING', vesselType: 'Cargo', flagState: 'Ethiopia', grossTonnage: 1840, ownerName: 'Horn Shipping Ltd', status: 'REGISTERED', registeredAt: '2026-08-16T08:20:00.000Z' },
|
||||
{ registrationNumber: 'ET-VR-004611', name: 'Tana Breeze', category: 'INLAND_WATERWAY', vesselType: 'Passenger ferry', flagState: 'Ethiopia', grossTonnage: 96, ownerName: 'Lake Tana Ferries', status: 'REGISTERED', registeredAt: '2026-08-15T13:05:00.000Z' },
|
||||
{ registrationNumber: 'ET-VR-004610', name: 'Baro Voyager', category: 'INLAND_WATERWAY', vesselType: 'Barge', flagState: 'Ethiopia', grossTonnage: 240, ownerName: 'Baro River Logistics', status: 'REGISTERED', registeredAt: '2026-08-14T10:41:00.000Z' },
|
||||
{ registrationNumber: 'ET-VR-004609', name: 'Red Sea Falcon', category: 'SEA_GOING', vesselType: 'Tanker', flagState: 'Djibouti', grossTonnage: 3210, ownerName: 'Horn Shipping Ltd', status: 'SUSPENDED', registeredAt: '2026-08-12T15:32:00.000Z' },
|
||||
{ registrationNumber: 'ET-VR-004608', name: 'Hawassa Queen', category: 'INLAND_WATERWAY', vesselType: 'Passenger ferry', flagState: 'Ethiopia', grossTonnage: 74, ownerName: 'Hawassa Boat Union', status: 'REGISTERED', registeredAt: '2026-08-11T09:12:00.000Z' },
|
||||
{ registrationNumber: 'ET-VR-004607', name: 'Omo Spirit', category: 'INLAND_WATERWAY', vesselType: 'Fishing', flagState: 'Ethiopia', grossTonnage: 32, ownerName: 'Omo Cargo Co.', status: 'REGISTERED', registeredAt: '2026-08-09T07:55:00.000Z' },
|
||||
{ registrationNumber: 'ET-VR-004606', name: 'Awash Pioneer', category: 'SEA_GOING', vesselType: 'Cargo', flagState: 'Panama', grossTonnage: 2105, ownerName: 'Awash Marine Services', status: 'REGISTERED', registeredAt: '2026-08-07T11:26:00.000Z' },
|
||||
{ registrationNumber: 'ET-VR-004605', name: 'Gibe Runner', category: 'INLAND_WATERWAY', vesselType: 'Tug', flagState: 'Ethiopia', grossTonnage: 118, ownerName: 'Gibe Marine PLC', status: 'DEREGISTERED', registeredAt: '2026-08-04T14:03:00.000Z' },
|
||||
] as const).map((row, i) => ({
|
||||
...row,
|
||||
vesselId: `9a0e0000-0000-4000-8000-0000000${3000 + i}`,
|
||||
})),
|
||||
recentIncidents: [
|
||||
{ registrationNumber: 'ET-VR-004182', vesselName: 'Abay Star', occurredAt: '2026-08-15T06:40:00.000Z', severity: 'Moderate', location: 'Lake Tana, near Gorgora', description: 'Engine room flooding after a seal failure; vessel towed to port under escort.', reportedByOfficer: true },
|
||||
{ registrationNumber: 'ET-VR-004609', vesselName: 'Red Sea Falcon', occurredAt: '2026-08-10T18:12:00.000Z', severity: 'Major', location: 'Gulf of Tadjoura', description: 'Collision with a moored barge during night manoeuvring. Hull plating breached above the waterline.', reportedByOfficer: true },
|
||||
{ registrationNumber: 'ET-VR-003907', vesselName: 'Tana Pearl', occurredAt: '2026-08-06T12:30:00.000Z', severity: 'Minor', location: 'Bahir Dar jetty', description: 'Passenger slipped on a wet gangway; treated on shore, no hospitalisation.', reportedByOfficer: false },
|
||||
{ registrationNumber: 'ET-VR-004411', vesselName: 'Gambella Express', occurredAt: '2026-07-29T09:05:00.000Z', severity: 'Moderate', location: 'Baro River, km 42', description: 'Grounded on a sandbar for six hours at low water. No pollution reported.', reportedByOfficer: false },
|
||||
{ registrationNumber: 'ET-VR-004098', vesselName: 'Rift Valley II', occurredAt: '2026-07-21T16:48:00.000Z', severity: 'Critical', location: 'Lake Hawassa, south shore', description: 'Fire in the galley spread to the passenger deck; 3 injured, vessel out of service.', reportedByOfficer: true },
|
||||
{ registrationNumber: 'ET-VR-004320', vesselName: 'Omo Trader', occurredAt: '2026-07-14T05:15:00.000Z', severity: 'Minor', location: 'Omo River delta', description: 'Steering hydraulics lost pressure; repaired at berth the same day.', reportedByOfficer: false },
|
||||
].map((row, i) => ({
|
||||
...row,
|
||||
id: `1c2d0000-0000-4000-8000-0000000${4000 + i}`,
|
||||
vesselId: `9a0e0000-0000-4000-8000-0000000${1000 + i}`,
|
||||
})),
|
||||
pendingApplications: ([
|
||||
{ applicationNumber: 'VR-2026-000841', status: 'UNDER_REVIEW', kind: 'NEW', submittedAt: '2026-08-01T08:00:00.000Z', adjustmentRound: 0, daysOpen: 17 },
|
||||
{ applicationNumber: 'VR-2026-000836', status: 'CORRECTION_REQUIRED', kind: 'RENEWAL', submittedAt: '2026-07-28T10:22:00.000Z', adjustmentRound: 2, daysOpen: 21 },
|
||||
{ applicationNumber: 'VR-2026-000829', status: 'UNDER_REVIEW', kind: 'NEW', submittedAt: '2026-07-24T09:14:00.000Z', adjustmentRound: 1, daysOpen: 25 },
|
||||
{ applicationNumber: 'VR-2026-000818', status: 'RESUBMITTED', kind: 'NEW', submittedAt: '2026-07-19T13:40:00.000Z', adjustmentRound: 1, daysOpen: 30 },
|
||||
{ applicationNumber: 'VR-2026-000804', status: 'UNDER_REVIEW', kind: 'RENEWAL', submittedAt: '2026-07-11T07:52:00.000Z', adjustmentRound: 0, daysOpen: 38 },
|
||||
{ applicationNumber: 'VR-2026-000791', status: 'CORRECTION_REQUIRED', kind: 'NEW', submittedAt: '2026-07-02T15:31:00.000Z', adjustmentRound: 3, daysOpen: 47 },
|
||||
{ applicationNumber: 'VR-2026-000783', status: 'SUBMITTED', kind: 'RENEWAL', submittedAt: '2026-06-26T11:09:00.000Z', adjustmentRound: 0, daysOpen: 53 },
|
||||
] as const).map((row, i) => ({
|
||||
...row,
|
||||
assignedOfficerId:
|
||||
i % 3 === 2 ? null : `5f1c1b8e-000${(i % 5) + 1}-4a11-9c31-0a1b2c3d4e0${(i % 5) + 1}`,
|
||||
})),
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user