From 2be18764602206222a0fc79d2914c92007c4a244 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Mon, 24 Aug 2026 07:12:08 +0000 Subject: [PATCH] feat(reports): carry time of day on report date columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eleven report columns bucketed their timestamp to a bare day with to_char, which is wrong for anything a user reads as an event rather than a period: two departures on the same date, or a wagon request fulfilled hours after it was raised, were indistinguishable in the output. The renderer only shows the time when the value actually has one, keyed off the string rather than a per-column flag — a genuine day bucket would otherwise render as 12:00 AM, which reads as data rather than as absence. --- .../charged-vs-actual-volume.report.ts | 2 +- .../definitions/contract-utilization.report.ts | 4 ++-- .../first-last-mile-bookings.report.ts | 2 +- .../reports/definitions/loaded-capacity.report.ts | 2 +- .../definitions/receivables-payables.report.ts | 2 +- .../definitions/revenue-reconciliation.report.ts | 2 +- .../definitions/revenue-transactions.report.ts | 2 +- .../definitions/train-schedule-status.report.ts | 2 +- .../reports/definitions/wagon-requests.report.ts | 4 ++-- .../definitions/wagon-status-duration.report.ts | 2 +- .../definitions/wagon-teu-utilization.report.ts | 2 +- .../src/components/reports/report-format.ts | 15 +++++++++++---- 12 files changed, 24 insertions(+), 17 deletions(-) diff --git a/apps/edr-freight-api/src/modules/reports/definitions/charged-vs-actual-volume.report.ts b/apps/edr-freight-api/src/modules/reports/definitions/charged-vs-actual-volume.report.ts index 6da342e17..e495f409b 100644 --- a/apps/edr-freight-api/src/modules/reports/definitions/charged-vs-actual-volume.report.ts +++ b/apps/edr-freight-api/src/modules/reports/definitions/charged-vs-actual-volume.report.ts @@ -68,7 +68,7 @@ export const chargedVsActualVolumeReport: ReportDefinition = { query(ctx) { return baseQuery(ctx) .select("COALESCE(ts.train_number, '—')", 'trainNumber') - .addSelect(`to_char(COALESCE(ts.actual_departure_at, ts.scheduled_departure_date), 'YYYY-MM-DD')`, 'departedAt') + .addSelect(`to_char(COALESCE(ts.actual_departure_at, ts.scheduled_departure_date), 'YYYY-MM-DD HH24:MI')`, 'departedAt') .addSelect("COALESCE(oy.label, oy.code, '?') || ' → ' || COALESCE(dy.label, dy.code, '?')", 'station') .addSelect(CARGO_CATEGORY_LABEL_EXPR, 'category') .addSelect(`ROUND((${CHARGED_TONS_EXPR})::numeric, 2)::float8`, 'chargedTons') diff --git a/apps/edr-freight-api/src/modules/reports/definitions/contract-utilization.report.ts b/apps/edr-freight-api/src/modules/reports/definitions/contract-utilization.report.ts index 03f98d306..747a14891 100644 --- a/apps/edr-freight-api/src/modules/reports/definitions/contract-utilization.report.ts +++ b/apps/edr-freight-api/src/modules/reports/definitions/contract-utilization.report.ts @@ -91,8 +91,8 @@ export const contractUtilizationReport: ReportDefinition = { .addSelect('c.name', 'customer') .addSelect('ct.status', 'status') .addSelect('ct.contract_kind', 'kind') - .addSelect(`to_char(ct.contract_valid_from, 'YYYY-MM-DD')`, 'validFrom') - .addSelect(`to_char(ct.contract_valid_until, 'YYYY-MM-DD')`, 'validUntil') + .addSelect(`to_char(ct.contract_valid_from, 'YYYY-MM-DD HH24:MI')`, 'validFrom') + .addSelect(`to_char(ct.contract_valid_until, 'YYYY-MM-DD HH24:MI')`, 'validUntil') .addSelect('COALESCE(cap.committed, 0)::float8', 'committed') .addSelect('COALESCE(booked.tons, 0)::float8', 'bookedTons') .addSelect('COALESCE(booked.cnt, 0)', 'bookings') diff --git a/apps/edr-freight-api/src/modules/reports/definitions/first-last-mile-bookings.report.ts b/apps/edr-freight-api/src/modules/reports/definitions/first-last-mile-bookings.report.ts index d3abe10dc..06e26d246 100644 --- a/apps/edr-freight-api/src/modules/reports/definitions/first-last-mile-bookings.report.ts +++ b/apps/edr-freight-api/src/modules/reports/definitions/first-last-mile-bookings.report.ts @@ -72,7 +72,7 @@ export const firstLastMileBookingsReport: ReportDefinition = { .addSelect('fl.status', 'status') .addSelect("COALESCE(v.plate_number, '—')", 'truck') .addSelect("CASE WHEN fl.vehicle_id IS NOT NULL THEN 'Assigned' ELSE 'Unassigned' END", 'assigned') - .addSelect(`to_char(fl.created_at, 'YYYY-MM-DD')`, 'createdAt'); + .addSelect(`to_char(fl.created_at, 'YYYY-MM-DD HH24:MI')`, 'createdAt'); }, async summary(ctx) { const row = await baseQuery(ctx) diff --git a/apps/edr-freight-api/src/modules/reports/definitions/loaded-capacity.report.ts b/apps/edr-freight-api/src/modules/reports/definitions/loaded-capacity.report.ts index 8827b77b8..65dfada57 100644 --- a/apps/edr-freight-api/src/modules/reports/definitions/loaded-capacity.report.ts +++ b/apps/edr-freight-api/src/modules/reports/definitions/loaded-capacity.report.ts @@ -49,7 +49,7 @@ export const loadedCapacityReport: ReportDefinition = { query(ctx) { return baseQuery(ctx) .select('ts.train_number', 'trainNumber') - .addSelect(`to_char(ts.scheduled_departure_date, 'YYYY-MM-DD')`, 'departureDate') + .addSelect(`to_char(ts.scheduled_departure_date, 'YYYY-MM-DD HH24:MI')`, 'departureDate') .addSelect("COALESCE(wt.name, 'Unknown')", 'wagonType') .addSelect('COUNT(*)::int', 'wagons') .addSelect('COALESCE(SUM(tsw.capacity_tons), 0)::float8', 'capacityTons') diff --git a/apps/edr-freight-api/src/modules/reports/definitions/receivables-payables.report.ts b/apps/edr-freight-api/src/modules/reports/definitions/receivables-payables.report.ts index 0e07710bb..190e83c02 100644 --- a/apps/edr-freight-api/src/modules/reports/definitions/receivables-payables.report.ts +++ b/apps/edr-freight-api/src/modules/reports/definitions/receivables-payables.report.ts @@ -378,7 +378,7 @@ export const receivablesPayablesReport: ReportDefinition = { query(ctx) { return baseQuery(ctx) .select(SIDE_LABEL_OF('r.side_key'), 'side') - .addSelect("to_char(r.txn_date, 'YYYY-MM-DD')", 'issuedAt') + .addSelect("to_char(r.txn_date, 'YYYY-MM-DD HH24:MI')", 'issuedAt') .addSelect('r.doc_ref', 'invoiceNumber') .addSelect('r.booking_ref', 'bookingRef') .addSelect('r.booking_status', 'bookingStatus') diff --git a/apps/edr-freight-api/src/modules/reports/definitions/revenue-reconciliation.report.ts b/apps/edr-freight-api/src/modules/reports/definitions/revenue-reconciliation.report.ts index 73de3f505..c7bec9497 100644 --- a/apps/edr-freight-api/src/modules/reports/definitions/revenue-reconciliation.report.ts +++ b/apps/edr-freight-api/src/modules/reports/definitions/revenue-reconciliation.report.ts @@ -63,7 +63,7 @@ export const revenueReconciliationReport: ReportDefinition = { defaultSort: { key: 'variance', dir: 'DESC' }, query(ctx) { return baseQuery(ctx) - .select(`to_char(${REVENUE_DATE}, 'YYYY-MM-DD')`, 'issuedAt') + .select(`to_char(${REVENUE_DATE}, 'YYYY-MM-DD HH24:MI')`, 'issuedAt') .addSelect('i.invoice_number', 'invoiceNumber') .addSelect("COALESCE(b.reference, '—')", 'bookingRef') .addSelect(PAYER_EXPR, 'customer') diff --git a/apps/edr-freight-api/src/modules/reports/definitions/revenue-transactions.report.ts b/apps/edr-freight-api/src/modules/reports/definitions/revenue-transactions.report.ts index ad96b5b9e..044f0141e 100644 --- a/apps/edr-freight-api/src/modules/reports/definitions/revenue-transactions.report.ts +++ b/apps/edr-freight-api/src/modules/reports/definitions/revenue-transactions.report.ts @@ -93,7 +93,7 @@ export const revenueTransactionsReport: ReportDefinition = { defaultSort: { key: 'issuedAt', dir: 'DESC' }, query(ctx) { return baseQuery(ctx) - .select(`to_char(${REVENUE_DATE}, 'YYYY-MM-DD')`, 'issuedAt') + .select(`to_char(${REVENUE_DATE}, 'YYYY-MM-DD HH24:MI')`, 'issuedAt') .addSelect('i.invoice_number', 'invoiceNumber') .addSelect("COALESCE(b.reference, '—')", 'bookingRef') .addSelect("COALESCE(b.id::text, '')", 'bookingId') diff --git a/apps/edr-freight-api/src/modules/reports/definitions/train-schedule-status.report.ts b/apps/edr-freight-api/src/modules/reports/definitions/train-schedule-status.report.ts index 88a25d890..b81ea7896 100644 --- a/apps/edr-freight-api/src/modules/reports/definitions/train-schedule-status.report.ts +++ b/apps/edr-freight-api/src/modules/reports/definitions/train-schedule-status.report.ts @@ -76,7 +76,7 @@ export const trainScheduleStatusReport: ReportDefinition = { .addSelect('ts.direction', 'direction') .addSelect("COALESCE(o.label, 'Unknown')", 'origin') .addSelect("COALESCE(d.label, 'Unknown')", 'destination') - .addSelect(`to_char(ts.scheduled_departure_date, 'YYYY-MM-DD')`, 'scheduledDeparture') + .addSelect(`to_char(ts.scheduled_departure_date, 'YYYY-MM-DD HH24:MI')`, 'scheduledDeparture') .addSelect(`to_char(ts.actual_departure_at, 'YYYY-MM-DD HH24:MI')`, 'actualDeparture') .addSelect(`to_char(ts.actual_arrival_at, 'YYYY-MM-DD HH24:MI')`, 'actualArrival'); }, diff --git a/apps/edr-freight-api/src/modules/reports/definitions/wagon-requests.report.ts b/apps/edr-freight-api/src/modules/reports/definitions/wagon-requests.report.ts index df5ac364c..67a916d45 100644 --- a/apps/edr-freight-api/src/modules/reports/definitions/wagon-requests.report.ts +++ b/apps/edr-freight-api/src/modules/reports/definitions/wagon-requests.report.ts @@ -57,8 +57,8 @@ export const wagonRequestsReport: ReportDefinition = { .addSelect('r.quantity', 'quantity') .addSelect('r.fulfilled_quantity', 'fulfilledQuantity') .addSelect('r.status', 'status') - .addSelect(`to_char(r.created_at, 'YYYY-MM-DD')`, 'requestedAt') - .addSelect(`to_char(r.fulfilled_at, 'YYYY-MM-DD')`, 'fulfilledAt') + .addSelect(`to_char(r.created_at, 'YYYY-MM-DD HH24:MI')`, 'requestedAt') + .addSelect(`to_char(r.fulfilled_at, 'YYYY-MM-DD HH24:MI')`, 'fulfilledAt') .addSelect( `ROUND(EXTRACT(EPOCH FROM (COALESCE(r.fulfilled_at, now()) - r.created_at))::numeric / 86400, 1)::float8`, 'delayDays', diff --git a/apps/edr-freight-api/src/modules/reports/definitions/wagon-status-duration.report.ts b/apps/edr-freight-api/src/modules/reports/definitions/wagon-status-duration.report.ts index 348f681d1..771eec71d 100644 --- a/apps/edr-freight-api/src/modules/reports/definitions/wagon-status-duration.report.ts +++ b/apps/edr-freight-api/src/modules/reports/definitions/wagon-status-duration.report.ts @@ -69,7 +69,7 @@ export const wagonStatusDurationReport: ReportDefinition = { .addSelect("COALESCE(wt.name, 'Unknown')", 'wagonType') .addSelect("COALESCE(y.label, 'Unassigned')", 'station') .addSelect('w.status', 'status') - .addSelect(`to_char(COALESCE(log.since, w.updated_at), 'YYYY-MM-DD')`, 'since') + .addSelect(`to_char(COALESCE(log.since, w.updated_at), 'YYYY-MM-DD HH24:MI')`, 'since') .addSelect( `FLOOR(EXTRACT(EPOCH FROM (now() - COALESCE(log.since, w.updated_at))) / 86400)::int`, 'daysInStatus', diff --git a/apps/edr-freight-api/src/modules/reports/definitions/wagon-teu-utilization.report.ts b/apps/edr-freight-api/src/modules/reports/definitions/wagon-teu-utilization.report.ts index 7dabec37c..a10df9976 100644 --- a/apps/edr-freight-api/src/modules/reports/definitions/wagon-teu-utilization.report.ts +++ b/apps/edr-freight-api/src/modules/reports/definitions/wagon-teu-utilization.report.ts @@ -53,7 +53,7 @@ export const wagonTeuUtilizationReport: ReportDefinition = { .select('w.wagon_number', 'wagonNumber') .addSelect("COALESCE(wt.name, 'Unknown')", 'wagonType') .addSelect('ts.train_number', 'trainNumber') - .addSelect(`to_char(ts.scheduled_departure_date, 'YYYY-MM-DD')`, 'departureDate') + .addSelect(`to_char(ts.scheduled_departure_date, 'YYYY-MM-DD HH24:MI')`, 'departureDate') .addSelect('COUNT(c.id)::int', 'containers') .addSelect('(COALESCE(SUM(ct.size_ft), 0) / 20.0)::float8', 'teu') .groupBy('w.wagon_number') diff --git a/apps/edr-freight-web/backoffice/src/components/reports/report-format.ts b/apps/edr-freight-web/backoffice/src/components/reports/report-format.ts index d9b174ef1..15933ee78 100644 --- a/apps/edr-freight-web/backoffice/src/components/reports/report-format.ts +++ b/apps/edr-freight-web/backoffice/src/components/reports/report-format.ts @@ -17,10 +17,17 @@ export function formatReportCell(value: unknown, type: ReportColumnType): string case "number": return Number(value).toLocaleString(); case "date": { - const d = new Date(String(value)); - return Number.isNaN(d.getTime()) - ? String(value) - : d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); + const raw = String(value); + const d = new Date(raw); + if (Number.isNaN(d.getTime())) return raw; + // Day-bucket columns carry no time part — don't invent a 12:00 AM for them. + const hasTime = /\d:\d/.test(raw); + return d.toLocaleString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + ...(hasTime ? { hour: "2-digit", minute: "2-digit" } : {}), + }); } default: return String(value);