mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 15:25:45 +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.
80 lines
4.6 KiB
TypeScript
80 lines
4.6 KiB
TypeScript
import { FREIGHT_PERMS } from '../../../seed/freight-permissions.registry';
|
|
import { Locomotive } from '../../locomotives/entities/locomotive.entity';
|
|
import { Yard } from '../../rule-engine/entities/yard.entity';
|
|
import { ExportDataset } from '../export.types';
|
|
|
|
const STATUS_OPTIONS = ['AVAILABLE', 'IN_USE', 'MAINTENANCE', 'OUT_OF_SERVICE'].map((v) => ({
|
|
value: v,
|
|
label: v.replace(/_/g, ' '),
|
|
}));
|
|
|
|
export const locomotivesDataset: ExportDataset = {
|
|
key: 'locomotives',
|
|
title: 'Locomotives',
|
|
description: 'Locomotive fleet with capacity, traction specs and current location',
|
|
group: 'Fleet',
|
|
permission: FREIGHT_PERMS.locomotives.view,
|
|
base: { entity: Locomotive, alias: 'l' },
|
|
|
|
joins: [{ alias: 'y', entity: Yard, on: 'y.id = l.current_yard_id' }],
|
|
|
|
groups: [
|
|
{ id: 'identity', label: 'Locomotive' },
|
|
{ id: 'capacity', label: 'Capacity & traction' },
|
|
{ id: 'location', label: 'Status & location' },
|
|
{ id: 'assignment', label: 'Assignment' },
|
|
],
|
|
|
|
fields: [
|
|
{ key: 'code', label: 'Code', type: 'string', group: 'identity', default: true, select: 'l.code', sortExpr: 'l.code' },
|
|
{ key: 'name', label: 'Name', type: 'string', group: 'identity', default: true, select: 'l.name' },
|
|
{ key: 'locomotiveType', label: 'Type', type: 'string', group: 'identity', default: true, select: 'l.locomotive_type' },
|
|
{ key: 'createdAt', label: 'Added', type: 'date', group: 'identity', select: `to_char(l.created_at, 'YYYY-MM-DD')`, sortExpr: 'l.created_at' },
|
|
|
|
{ key: 'maxPullWeightTons', label: 'Max pull weight (t)', type: 'tons', group: 'capacity', default: true, select: 'l.max_pull_weight_tons::float8', sortExpr: 'l.max_pull_weight_tons' },
|
|
{ key: 'maxTrainLengthMeters', label: 'Max train length (m)', type: 'number', group: 'capacity', default: true, select: 'l.max_train_length_meters::float8' },
|
|
{ key: 'overageToleranceTons', label: 'Overage tolerance (t)', type: 'tons', group: 'capacity', select: 'l.overage_tolerance_tons::float8' },
|
|
{ key: 'overageToleranceMeters', label: 'Overage tolerance (m)', type: 'number', group: 'capacity', select: 'l.overage_tolerance_meters::float8' },
|
|
{ key: 'powerKw', label: 'Power (kW)', type: 'number', group: 'capacity', select: 'l.power_kw::float8' },
|
|
{ key: 'tractionForceKn', label: 'Traction force (kN)', type: 'number', group: 'capacity', select: 'l.traction_force_kn::float8' },
|
|
{ key: 'maxSpeedKmh', label: 'Max speed (km/h)', type: 'number', group: 'capacity', select: 'l.max_speed_kmh::float8' },
|
|
|
|
{ key: 'status', label: 'Status', type: 'string', group: 'location', default: true, select: 'l.status', sortExpr: 'l.status' },
|
|
{ key: 'availableFrom', label: 'Available from', type: 'date', group: 'location', select: `to_char(l.available_from, 'YYYY-MM-DD')` },
|
|
{ key: 'currentYard', label: 'Current yard', type: 'string', group: 'location', default: true, requires: ['y'], select: 'y.label' },
|
|
{ key: 'currentYardCode', label: 'Current yard code', type: 'string', group: 'location', requires: ['y'], select: 'y.code' },
|
|
{ key: 'currentYardCountry', label: 'Current yard country', type: 'string', group: 'location', requires: ['y'], select: 'y.country' },
|
|
|
|
{
|
|
// One-to-many -> aggregate in a subquery, never a join.
|
|
key: 'assignedTrains', label: 'Assigned trains', type: 'string', group: 'assignment',
|
|
select: `(SELECT string_agg(DISTINCT tr.code, ' | ')
|
|
FROM freight.train_set_locomotives tsl
|
|
JOIN freight.train_sets ts ON ts.id = tsl.train_set_id AND ts.deleted_at IS NULL
|
|
JOIN freight.trains tr ON tr.id = ts.train_id AND tr.deleted_at IS NULL
|
|
WHERE tsl.locomotive_id = l.id AND tsl.deleted_at IS NULL)`,
|
|
},
|
|
],
|
|
|
|
filters: [
|
|
{ key: 'status', label: 'Status', type: 'select', options: STATUS_OPTIONS },
|
|
{ key: 'locomotiveType', label: 'Type', type: 'text' },
|
|
{ key: 'currentYardId', label: 'Current yard', type: 'text' },
|
|
{ key: 'search', label: 'Search code or name', type: 'text' },
|
|
],
|
|
|
|
defaultSort: { key: 'code', dir: 'ASC' },
|
|
|
|
scope(ctx, qb) {
|
|
const { params } = ctx;
|
|
qb.andWhere('l.deleted_at IS NULL');
|
|
if (params.status) qb.andWhere('l.status = :status', { status: params.status });
|
|
if (params.locomotiveType) qb.andWhere('l.locomotive_type = :locomotiveType', { locomotiveType: params.locomotiveType });
|
|
if (params.currentYardId) qb.andWhere('l.current_yard_id = :currentYardId', { currentYardId: params.currentYardId });
|
|
if (params.search) {
|
|
qb.andWhere('(l.code ILIKE :search OR l.name ILIKE :search)', { search: `%${params.search as string}%` });
|
|
}
|
|
// Locomotives carry no trade direction — nothing to scope. Intentional.
|
|
},
|
|
};
|