import { FREIGHT_PERMS } from '../../../seed/freight-permissions.registry'; import { bookingTonsSql } from '../../bookings/booking-tons.sql'; import { Route } from '../../routes/entities/route.entity'; import { Yard } from '../../rule-engine/entities/yard.entity'; import { ShippingLineCompany } from '../../shipping-lines/entities/shipping-line-company.entity'; import { TrainSchedule } from '../../train-schedules/entities/train-schedule.entity'; import { applyDirectionScope } from '../../user-trade-access/trade-scope.util'; import { ExportDataset } from '../export.types'; /** * `freightType` is NOT a column on train_schedules — it is derived from the * bookings aboard (the list service does this with an EXISTS subquery). Exposed * here as an aggregate over those bookings, never as `sch.freight_type`. */ export const trainSchedulesDataset: ExportDataset = { key: 'train-schedules', title: 'Train schedules', description: 'Scheduled trains with route, timings, booking window and load', group: 'Operations', permission: FREIGHT_PERMS.trainScheduling.view, base: { entity: TrainSchedule, alias: 'sch' }, joins: [ { alias: 'rt', entity: Route, on: 'rt.id = sch.route_id' }, { alias: 'os', entity: Yard, on: 'os.id = sch.origin_station_id' }, { alias: 'ds', entity: Yard, on: 'ds.id = sch.destination_station_id' }, { alias: 'slc', entity: ShippingLineCompany, on: 'slc.id = sch.shipping_line_company_id' }, ], groups: [ { id: 'schedule', label: 'Schedule' }, { id: 'route', label: 'Route' }, { id: 'timings', label: 'Timings' }, { id: 'window', label: 'Booking window' }, { id: 'load', label: 'Load' }, ], fields: [ { key: 'reference', label: 'Reference', type: 'string', group: 'schedule', default: true, select: 'sch.reference', sortExpr: 'sch.reference' }, { key: 'status', label: 'Status', type: 'string', group: 'schedule', default: true, select: 'sch.status', sortExpr: 'sch.status' }, { key: 'trainNumber', label: 'Train number', type: 'string', group: 'schedule', default: true, select: 'sch.train_number' }, { key: 'voyageNumber', label: 'Voyage number', type: 'string', group: 'schedule', select: 'sch.voyage_number' }, { key: 'direction', label: 'Direction', type: 'string', group: 'schedule', default: true, select: 'sch.direction' }, { key: 'shippingLineCompany', label: 'Shipping line company', type: 'string', group: 'schedule', requires: ['slc'], select: 'slc.name' }, { key: 'bookingCycleNo', label: 'Booking cycle', type: 'number', group: 'schedule', select: 'sch.booking_cycle_no' }, { key: 'reverseWagonOrder', label: 'Reverse wagon order', type: 'boolean', group: 'schedule', select: 'sch.reverse_wagon_order' }, { key: 'createdAt', label: 'Created', type: 'date', group: 'schedule', select: `to_char(sch.created_at, 'YYYY-MM-DD')`, sortExpr: 'sch.created_at' }, { key: 'originStation', label: 'Origin', type: 'string', group: 'route', default: true, requires: ['os'], select: 'os.label' }, { key: 'originStationCode', label: 'Origin code', type: 'string', group: 'route', requires: ['os'], select: 'os.code' }, { key: 'destinationStation', label: 'Destination', type: 'string', group: 'route', default: true, requires: ['ds'], select: 'ds.label' }, { key: 'destinationStationCode', label: 'Destination code', type: 'string', group: 'route', requires: ['ds'], select: 'ds.code' }, { key: 'routeStatus', label: 'Route status', type: 'string', group: 'route', requires: ['rt'], select: 'rt.status' }, { key: 'scheduledDeparture', label: 'Scheduled departure', type: 'datetime', group: 'timings', default: true, select: `to_char(sch.scheduled_departure_date, 'YYYY-MM-DD HH24:MI')`, sortExpr: 'sch.scheduled_departure_date' }, { key: 'scheduledArrival', label: 'Scheduled arrival', type: 'datetime', group: 'timings', default: true, select: `to_char(sch.scheduled_arrival_date, 'YYYY-MM-DD HH24:MI')` }, { key: 'actualDeparture', label: 'Actual departure', type: 'datetime', group: 'timings', select: `to_char(sch.actual_departure_at, 'YYYY-MM-DD HH24:MI')` }, { key: 'actualArrival', label: 'Actual arrival', type: 'datetime', group: 'timings', select: `to_char(sch.actual_arrival_at, 'YYYY-MM-DD HH24:MI')` }, { key: 'departureDelayHours', label: 'Departure delay (h)', type: 'number', group: 'timings', select: `ROUND(EXTRACT(EPOCH FROM (sch.actual_departure_at - sch.scheduled_departure_date)) / 3600.0, 2)::float8`, }, { key: 'transitHours', label: 'Transit time (h)', type: 'number', group: 'timings', select: `ROUND(EXTRACT(EPOCH FROM (sch.actual_arrival_at - sch.actual_departure_at)) / 3600.0, 2)::float8`, }, { key: 'bookingWindowStatus', label: 'Window status', type: 'string', group: 'window', select: 'sch.booking_window_status' }, { key: 'windowPhase', label: 'Window phase', type: 'string', group: 'window', select: 'sch.window_phase' }, { key: 'windowOpensAt', label: 'Window opens', type: 'datetime', group: 'window', select: `to_char(sch.window_opens_at, 'YYYY-MM-DD HH24:MI')` }, { key: 'windowClosesAt', label: 'Window closes', type: 'datetime', group: 'window', select: `to_char(sch.window_closes_at, 'YYYY-MM-DD HH24:MI')` }, { key: 'docReviewEndsAt', label: 'Doc review ends', type: 'datetime', group: 'window', select: `to_char(sch.doc_review_ends_at, 'YYYY-MM-DD HH24:MI')` }, { key: 'paymentPhaseEndsAt', label: 'Payment phase ends', type: 'datetime', group: 'window', select: `to_char(sch.payment_phase_ends_at, 'YYYY-MM-DD HH24:MI')` }, { key: 'windowRuleCustom', label: 'Custom window rules', type: 'boolean', group: 'window', select: 'sch.window_rule_custom' }, { key: 'maxWagons', label: 'Max wagons', type: 'number', group: 'load', select: 'sch.max_wagons' }, { key: 'bookingCount', label: 'Bookings', type: 'number', group: 'load', default: true, select: `(SELECT COUNT(*)::int FROM freight.bookings b WHERE b.train_schedule_id = sch.id AND b.deleted_at IS NULL)`, }, { // Derived, not a column — see the file header. key: 'freightTypes', label: 'Freight types', type: 'string', group: 'load', default: true, select: `(SELECT string_agg(DISTINCT b.freight_type, ' | ') FROM freight.bookings b WHERE b.train_schedule_id = sch.id AND b.deleted_at IS NULL)`, }, { key: 'totalWeightTons', label: 'Total weight (t)', type: 'tons', group: 'load', default: true, select: `(SELECT ROUND(COALESCE(SUM(${bookingTonsSql('b')}), 0))::float8 FROM freight.bookings b WHERE b.train_schedule_id = sch.id AND b.deleted_at IS NULL)`, }, { key: 'assignedWagonCount', label: 'Wagons assigned', type: 'number', group: 'load', select: `(SELECT COUNT(*)::int FROM freight.wagons w WHERE w.current_train_schedule_id = sch.id AND w.deleted_at IS NULL)`, }, ], filters: [ { key: 'departure', label: 'Departure', type: 'daterange' }, { key: 'status', label: 'Status', type: 'text' }, { key: 'direction', label: 'Direction', type: 'select', options: ['IMPORT', 'EXPORT', 'DOMESTIC'].map((v) => ({ value: v, label: v })) }, // freightType is derived from the bookings aboard, so it filters via // EXISTS — the same shape the list service's scheduleFreightTypeFilter uses. { key: 'freightType', label: 'Freight type', type: 'select', options: ['CONTAINER', 'BULK'].map((v) => ({ value: v, label: v })) }, { key: 'originStationId', label: 'Origin', type: 'text' }, { key: 'destinationStationId', label: 'Destination', type: 'text' }, { key: 'search', label: 'Search reference or train number', type: 'text' }, ], defaultSort: { key: 'scheduledDeparture', dir: 'DESC' }, scope(ctx, qb) { const { params, directions } = ctx; qb.andWhere('sch.deleted_at IS NULL'); if (params.departureFrom) qb.andWhere('sch.scheduled_departure_date >= :departureFrom', { departureFrom: params.departureFrom }); if (params.departureTo) qb.andWhere('sch.scheduled_departure_date < :departureTo', { departureTo: params.departureTo }); if (params.status) qb.andWhere('sch.status = :status', { status: params.status }); if (params.direction) qb.andWhere('sch.direction = :direction', { direction: params.direction }); if (params.freightType) { qb.andWhere( `EXISTS (SELECT 1 FROM freight.bookings fb WHERE fb.train_schedule_id = sch.id AND fb.deleted_at IS NULL AND fb.freight_type = :freightType)`, { freightType: params.freightType }, ); } if (params.originStationId) qb.andWhere('sch.origin_station_id = :originStationId', { originStationId: params.originStationId }); if (params.destinationStationId) qb.andWhere('sch.destination_station_id = :destinationStationId', { destinationStationId: params.destinationStationId }); if (params.search) { qb.andWhere('(sch.reference ILIKE :search OR sch.train_number ILIKE :search)', { search: `%${params.search as string}%` }); } // Schedules carry their own `direction` column, so scope on that directly // rather than through the bookings aboard. applyDirectionScope(qb, 'sch.direction', directions); }, };