mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
customers, contracts, invoices, payments, train-schedules, locomotives, trains and wagons. 319 fields across the nine datasets, all reusing the existing engine — no change to export.types.ts was needed, which is the result the bookings-first phase was meant to test. Per-dataset notes worth keeping: - trains resolves route, stations and current yard, which the list endpoint never loads — the UI shows raw FK uuids there today. - wagons reads tare/payload/length off wagon_types (they are not on the wagon), and reproduces the service's attachStatusDates() as correlated subqueries. wagon_status_logs stores from_status/to_status, not status. - payments applies no soft-delete guard: freight.payments has neither deleted_at nor updated_at, so the usual predicate is a 42703. Failure columns are failer_code/failer_message. payment_refunds stores MINOR units, so refundedTotal divides by 100. - train-schedules derives freightType from the bookings aboard rather than a column, matching the list service. - customers stays one row per company; profiles, bookings and invoice totals aggregate in subqueries. Verified no row multiplication: trains, customers and contracts each return exactly their counted row count while selecting one-to-many aggregate fields. EXPLAIN-validated against the database: every dataset's widest query, its count query, and all 319 fields individually. That run caught five columns typed varchar rather than timestamp (companies.date_registered, renewal_date, renewed_from, renewed_to and invoices.eims_ack_date), which were being pushed through to_char and would have 500'd the moment anyone ticked them; they now export verbatim. All nine count endpoints verified equal to SELECT count(*) on their table.
91 lines
5.0 KiB
TypeScript
91 lines
5.0 KiB
TypeScript
import { FREIGHT_PERMS } from '../../../seed/freight-permissions.registry';
|
|
import { Route } from '../../routes/entities/route.entity';
|
|
import { Yard } from '../../rule-engine/entities/yard.entity';
|
|
import { Train } from '../../trains/entities/train.entity';
|
|
import { ExportDataset } from '../export.types';
|
|
|
|
/**
|
|
* The list endpoint (`trains.service.ts findAll`) loads NO relations, so the
|
|
* UI shows raw FK uuids where names belong. This dataset resolves them, which
|
|
* makes it the clearest "the export shows more than the screen" case in the set.
|
|
*/
|
|
export const trainsDataset: ExportDataset = {
|
|
key: 'trains',
|
|
title: 'Trains',
|
|
description: 'Trains with route, stations, capacity and current composition',
|
|
group: 'Fleet',
|
|
permission: FREIGHT_PERMS.trains.view,
|
|
base: { entity: Train, alias: 't' },
|
|
|
|
joins: [
|
|
{ alias: 'y', entity: Yard, on: 'y.id = t.current_yard_id' },
|
|
{ alias: 'rt', entity: Route, on: 'rt.id = t.route_id' },
|
|
{ alias: 'os', entity: Yard, on: 'os.id = t.origin_station_id' },
|
|
{ alias: 'ds', entity: Yard, on: 'ds.id = t.destination_station_id' },
|
|
],
|
|
|
|
groups: [
|
|
{ id: 'train', label: 'Train' },
|
|
{ id: 'route', label: 'Route & stations' },
|
|
{ id: 'capacity', label: 'Capacity' },
|
|
{ id: 'composition', label: 'Composition' },
|
|
],
|
|
|
|
fields: [
|
|
{ key: 'code', label: 'Code', type: 'string', group: 'train', default: true, select: 't.code', sortExpr: 't.code' },
|
|
{ key: 'trainNumber', label: 'Train number', type: 'string', group: 'train', default: true, select: 't.train_number' },
|
|
{ key: 'trainName', label: 'Train name', type: 'string', group: 'train', default: true, select: 't.train_name' },
|
|
{ key: 'status', label: 'Status', type: 'string', group: 'train', default: true, select: 't.status', sortExpr: 't.status' },
|
|
{ key: 'importTrainNumber', label: 'Import run', type: 'string', group: 'train', select: 't.import_train_number' },
|
|
{ key: 'exportTrainNumber', label: 'Export run', type: 'string', group: 'train', select: 't.export_train_number' },
|
|
{ key: 'locomotiveNumber', label: 'Locomotive number', type: 'string', group: 'train', select: 't.locomotive_number' },
|
|
{ key: 'notes', label: 'Notes', type: 'string', group: 'train', select: 't.notes' },
|
|
{ key: 'remarks', label: 'Remarks', type: 'string', group: 'train', select: 't.remarks' },
|
|
{ key: 'createdAt', label: 'Added', type: 'date', group: 'train', select: `to_char(t.created_at, 'YYYY-MM-DD')`, sortExpr: 't.created_at' },
|
|
|
|
{ key: 'currentYard', label: 'Current yard', type: 'string', group: 'route', default: true, requires: ['y'], select: 'y.label' },
|
|
{ key: 'currentYardCode', label: 'Current yard code', type: 'string', group: 'route', requires: ['y'], select: 'y.code' },
|
|
{ key: 'routeDirection', label: 'Route direction', type: 'string', group: 'route', requires: ['rt'], select: 'rt.direction' },
|
|
{ key: 'routeStatus', label: 'Route status', type: 'string', group: 'route', requires: ['rt'], select: 'rt.status' },
|
|
{ key: 'originStation', label: 'Origin station', type: 'string', group: 'route', requires: ['os'], select: 'os.label' },
|
|
{ key: 'destinationStation', label: 'Destination station', type: 'string', group: 'route', requires: ['ds'], select: 'ds.label' },
|
|
{ key: 'departureTime', label: 'Departure time', type: 'string', group: 'route', select: 't.departure_time::text' },
|
|
{ key: 'arrivalTime', label: 'Arrival time', type: 'string', group: 'route', select: 't.arrival_time::text' },
|
|
|
|
{ key: 'capacityTons', label: 'Capacity (t)', type: 'tons', group: 'capacity', default: true, select: 't.capacity_tons::float8', sortExpr: 't.capacity_tons' },
|
|
|
|
{
|
|
key: 'wagonCount', label: 'Wagons attached', type: 'number', group: 'composition', default: true,
|
|
select: `(SELECT COUNT(*)::int FROM freight.wagons w
|
|
WHERE w.train_id = t.id AND w.deleted_at IS NULL)`,
|
|
},
|
|
{
|
|
key: 'wagonNumbers', label: 'Wagon numbers', type: 'string', group: 'composition',
|
|
select: `(SELECT string_agg(w.wagon_number, ' | ' ORDER BY w.sequence_number NULLS LAST, w.wagon_number)
|
|
FROM freight.wagons w
|
|
WHERE w.train_id = t.id AND w.deleted_at IS NULL)`,
|
|
},
|
|
],
|
|
|
|
filters: [
|
|
{ key: 'status', label: 'Status', type: 'text' },
|
|
{ key: 'currentYardId', label: 'Current yard', type: 'text' },
|
|
{ key: 'search', label: 'Search code, number or name', type: 'text' },
|
|
],
|
|
|
|
defaultSort: { key: 'code', dir: 'ASC' },
|
|
|
|
scope(ctx, qb) {
|
|
const { params } = ctx;
|
|
qb.andWhere('t.deleted_at IS NULL');
|
|
if (params.status) qb.andWhere('t.status = :status', { status: params.status });
|
|
if (params.currentYardId) qb.andWhere('t.current_yard_id = :currentYardId', { currentYardId: params.currentYardId });
|
|
if (params.search) {
|
|
qb.andWhere('(t.code ILIKE :search OR t.train_number ILIKE :search OR t.train_name ILIKE :search)', {
|
|
search: `%${params.search as string}%`,
|
|
});
|
|
}
|
|
// Trains carry no trade direction — nothing to scope. Intentional.
|
|
},
|
|
};
|