import { FREIGHT_PERMS } from '../../../seed/freight-permissions.registry'; import { Booking } from '../../bookings/entities/booking.entity'; import { Company } from '../../companies/entities/company.entity'; import { CompanyProfile } from '../../companies/entities/company-profile.entity'; import { Contract } from '../../contracts/entities/contract.entity'; import { CargoType } from '../../rule-engine/entities/cargo-type.entity'; import { ServiceType } from '../../rule-engine/entities/service-type.entity'; import { ShippingLine } from '../../rule-engine/entities/shipping-line.entity'; import { Yard } from '../../rule-engine/entities/yard.entity'; import { ShippingLineCompany } from '../../shipping-lines/entities/shipping-line-company.entity'; import { Train } from '../../trains/entities/train.entity'; import { applyDirectionScope } from '../../user-trade-access/trade-scope.util'; import { ExportDataset } from '../export.types'; /** * Domain semantics shared with `reports/definitions/bookings-list.report.ts`. * Kept identical on purpose — for PER_ITEM bulk bookings `cargo_total_weight_vgm` * holds an item COUNT, not tonnage, and `adjusted_total_amount` silently * overrides `total_amount`. Getting either wrong misreports money or weight. */ const TONS = 'COALESCE(b.bulk_total_weight_tons, b.cargo_total_weight_vgm)'; const REVENUE = 'COALESCE(b.adjusted_total_amount, b.total_amount)'; const STATUS_OPTIONS = [ 'DRAFT', 'SUBMITTED', 'UNDER_REVIEW', 'APPROVED', 'REJECTED', 'CANCELLED', 'EXPIRED', 'SCHEDULED', 'LOADED', 'IN_TRANSIT', 'ARRIVED', 'DELIVERED', 'COMPLETED', ].map((v) => ({ value: v, label: v.replace(/_/g, ' ') })); export const bookingsDataset: ExportDataset = { key: 'bookings', title: 'Bookings', description: 'Every booking, with customer, route, cargo, contract and payment detail', group: 'Commercial', permission: FREIGHT_PERMS.bookings.view, base: { entity: Booking, alias: 'b' }, // Every join is a LEFT join (see ExportJoin) — ticking a field must never // change which rows come back. joins: [ { alias: 'c', entity: Company, on: 'c.id = b.company_id' }, { alias: 'cp', entity: CompanyProfile, on: 'cp.id = b.company_profile_id' }, { alias: 'slc', entity: ShippingLineCompany, on: 'slc.id = b.shipping_line_company_id' }, { alias: 'o', entity: Yard, on: 'o.id = b.origin_yard_id' }, { alias: 'd', entity: Yard, on: 'd.id = b.destination_yard_id' }, { alias: 'cty', entity: CargoType, on: 'cty.id = b.cargo_type_id' }, { alias: 'st', entity: ServiceType, on: 'st.id = b.service_type_id' }, { alias: 'sl', entity: ShippingLine, on: 'sl.id = b.shipping_line_id' }, { alias: 'ct', entity: Contract, on: 'ct.id = b.contract_id' }, { alias: 't', entity: Train, on: 't.id = b.train_id' }, // Transitive: the contract's own customer, reachable only once `ct` is in. { alias: 'ctc', entity: Company, on: 'ctc.id = ct.company_id', requires: ['ct'] }, ], // `search` matches the customer name, so `c` is always present — which is // also why the count query joins it. alwaysJoin: ['c'], groups: [ { id: 'booking', label: 'Booking' }, { id: 'customer', label: 'Customer' }, { id: 'route', label: 'Route' }, { id: 'cargo', label: 'Cargo' }, { id: 'payment', label: 'Payment' }, { id: 'scheduling', label: 'Scheduling' }, { id: 'contract', label: 'Contract' }, { id: 'firstMile', label: 'First mile' }, { id: 'lastMile', label: 'Last mile' }, { id: 'clearance', label: 'Clearance' }, ], fields: [ // ---- Booking ------------------------------------------------------- { key: 'reference', label: 'Reference', type: 'string', group: 'booking', default: true, select: 'b.reference', sortExpr: 'b.reference' }, { key: 'status', label: 'Status', type: 'string', group: 'booking', default: true, select: 'b.status', sortExpr: 'b.status' }, { key: 'bookingType', label: 'Booking type', type: 'string', group: 'booking', select: 'b.booking_type' }, { key: 'contractKind', label: 'Contract kind', type: 'string', group: 'booking', select: 'b.contract_kind' }, { key: 'createdAt', label: 'Created', type: 'datetime', group: 'booking', default: true, select: `to_char(b.created_at, 'YYYY-MM-DD HH24:MI')`, sortExpr: 'b.created_at' }, { key: 'updatedAt', label: 'Updated', type: 'datetime', group: 'booking', select: `to_char(b.updated_at, 'YYYY-MM-DD HH24:MI')`, sortExpr: 'b.updated_at' }, { key: 'expiresAt', label: 'Expires', type: 'date', group: 'booking', select: `to_char(b.expires_at, 'YYYY-MM-DD')` }, { key: 'createdByRole', label: 'Created by role', type: 'string', group: 'booking', select: 'b.created_by_role' }, { key: 'isSplit', label: 'Split booking', type: 'boolean', group: 'booking', select: 'b.is_split' }, { key: 'priorityScore', label: 'Priority score', type: 'number', group: 'booking', select: 'b.priority_score', sortExpr: 'b.priority_score' }, { key: 'versionNumber', label: 'Version', type: 'number', group: 'booking', select: 'b.version_number' }, { key: 'pnrCode', label: 'PNR code', type: 'string', group: 'booking', select: 'b.pnr_code' }, // ---- Customer (the "more than the UI shows" payload) ---------------- { key: 'customer', label: 'Customer', type: 'string', group: 'customer', default: true, requires: ['c'], select: 'c.name', sortExpr: 'c.name' }, { key: 'customerType', label: 'Customer type', type: 'string', group: 'customer', requires: ['c'], select: 'c.type' }, { key: 'customerKind', label: 'Customer kind', type: 'string', group: 'customer', requires: ['c'], select: 'c.kind' }, { key: 'customerStatus', label: 'Customer status', type: 'string', group: 'customer', requires: ['c'], select: 'c.status' }, { key: 'customerTin', label: 'Customer TIN', type: 'string', group: 'customer', requires: ['c'], select: 'c.tin' }, { key: 'customerVat', label: 'Customer VAT no.', type: 'string', group: 'customer', requires: ['c'], select: 'c.vat_number' }, { key: 'customerPhone', label: 'Customer phone', type: 'string', group: 'customer', requires: ['c'], select: 'c.phone' }, { key: 'customerEmail', label: 'Customer email', type: 'string', group: 'customer', requires: ['c'], select: 'c.email' }, { key: 'customerContact', label: 'Contact person', type: 'string', group: 'customer', requires: ['c'], select: 'c.contact_person_name' }, { key: 'customerContactPhone', label: 'Contact phone', type: 'string', group: 'customer', requires: ['c'], select: 'c.contact_person_phone' }, { key: 'customerAddress', label: 'Customer address', type: 'string', group: 'customer', requires: ['c'], select: 'c.address' }, { key: 'customerCountry', label: 'Customer country', type: 'string', group: 'customer', requires: ['c'], select: 'c.country' }, { key: 'customerRegion', label: 'Customer region', type: 'string', group: 'customer', requires: ['c'], select: 'c.region' }, { key: 'customerProfileRef', label: 'Profile reference', type: 'string', group: 'customer', requires: ['cp'], select: 'cp.reference' }, { key: 'customerProfileType', label: 'Profile type', type: 'string', group: 'customer', requires: ['cp'], select: 'cp.type' }, { key: 'isGovernment', label: 'Government', type: 'boolean', group: 'customer', select: 'b.is_government' }, { key: 'governmentInstitution', label: 'Government institution', type: 'string', group: 'customer', select: 'b.government_institution' }, { key: 'shippingLineCompany', label: 'Shipping line company', type: 'string', group: 'customer', requires: ['slc'], select: 'slc.name' }, // ---- Route ---------------------------------------------------------- { key: 'origin', label: 'Origin', type: 'string', group: 'route', default: true, requires: ['o'], select: 'o.label' }, { key: 'originCode', label: 'Origin code', type: 'string', group: 'route', requires: ['o'], select: 'o.code' }, { key: 'destination', label: 'Destination', type: 'string', group: 'route', default: true, requires: ['d'], select: 'd.label' }, { key: 'destinationCode', label: 'Destination code', type: 'string', group: 'route', requires: ['d'], select: 'd.code' }, { key: 'tradeDirection', label: 'Direction', type: 'string', group: 'route', default: true, select: 'b.trade_direction', sortExpr: 'b.trade_direction' }, { key: 'serviceType', label: 'Service type', type: 'string', group: 'route', requires: ['st'], select: 'st.service_name' }, // ---- Cargo ----------------------------------------------------------- { key: 'cargo', label: 'Cargo', type: 'string', group: 'cargo', default: true, requires: ['cty'], select: 'COALESCE(cty.cargo_type_name, b.cargo_free_text)' }, { key: 'freightType', label: 'Freight type', type: 'string', group: 'cargo', default: true, select: 'b.freight_type' }, { key: 'tons', label: 'Tonnage', type: 'tons', group: 'cargo', default: true, select: `ROUND(${TONS})::float8`, sortExpr: TONS }, { key: 'containerWeightVgm', label: 'Container VGM', type: 'number', group: 'cargo', select: 'b.cargo_total_weight_vgm' }, { key: 'bulkWeightTons', label: 'Bulk weight (t)', type: 'tons', group: 'cargo', select: 'b.bulk_total_weight_tons' }, { key: 'isHazardous', label: 'Hazardous', type: 'boolean', group: 'cargo', select: 'b.is_hazardous' }, { key: 'isReefer', label: 'Reefer', type: 'boolean', group: 'cargo', select: 'b.is_reefer' }, { key: 'shippingLine', label: 'Shipping line', type: 'string', group: 'cargo', requires: ['sl'], select: 'sl.label' }, { // One-to-many, so it aggregates in a correlated subquery rather than a // join — a join here would multiply rows and break the count contract. key: 'containerNumbers', label: 'Container numbers', type: 'string', group: 'cargo', select: `(SELECT string_agg(bc.container_number, ' | ' ORDER BY bc.container_number) FROM freight.booking_container bc WHERE bc.booking_id = b.id AND bc.deleted_at IS NULL)`, }, // ---- Payment ---------------------------------------------------------- { key: 'amount', label: 'Amount', type: 'money', group: 'payment', default: true, select: `ROUND(${REVENUE}, 2)::float8`, sortExpr: REVENUE }, { key: 'totalAmount', label: 'Total amount (pre-adjustment)', type: 'money', group: 'payment', select: 'b.total_amount::float8' }, { key: 'adjustedTotalAmount', label: 'Adjusted total', type: 'money', group: 'payment', select: 'b.adjusted_total_amount::float8' }, { key: 'adjustmentReason', label: 'Adjustment reason', type: 'string', group: 'payment', select: 'b.adjustment_reason' }, { key: 'paymentStatus', label: 'Payment status', type: 'string', group: 'payment', default: true, select: 'b.payment_status', sortExpr: 'b.payment_status' }, { key: 'paymentCurrency', label: 'Currency', type: 'string', group: 'payment', select: 'b.payment_currency' }, { key: 'paymentDeadline', label: 'Payment deadline', type: 'datetime', group: 'payment', select: `to_char(b.payment_deadline, 'YYYY-MM-DD HH24:MI')` }, // ---- Scheduling -------------------------------------------------------- { key: 'scheduledDate', label: 'Scheduled date', type: 'date', group: 'scheduling', default: true, select: `to_char(b.scheduled_date, 'YYYY-MM-DD')`, sortExpr: 'b.scheduled_date' }, { key: 'schedulingStatus', label: 'Scheduling status', type: 'string', group: 'scheduling', select: 'b.scheduling_status' }, { key: 'wagonsRequired', label: 'Wagons required', type: 'number', group: 'scheduling', select: 'b.wagons_required' }, { key: 'trainCode', label: 'Train', type: 'string', group: 'scheduling', requires: ['t'], select: 't.code' }, { key: 'loadedAt', label: 'Loaded at', type: 'datetime', group: 'scheduling', select: `to_char(b.loaded_at, 'YYYY-MM-DD HH24:MI')` }, { key: 'arrivedAt', label: 'Arrived at', type: 'datetime', group: 'scheduling', select: `to_char(b.arrived_at, 'YYYY-MM-DD HH24:MI')` }, // ---- Contract ---------------------------------------------------------- { key: 'contractReference', label: 'Contract reference', type: 'string', group: 'contract', requires: ['ct'], select: 'ct.reference' }, { key: 'contractStatus', label: 'Contract status', type: 'string', group: 'contract', requires: ['ct'], select: 'ct.status' }, { key: 'contractCustomer', label: 'Contract customer', type: 'string', group: 'contract', requires: ['ctc'], select: 'ctc.name' }, { key: 'contractType', label: 'Contract type', type: 'string', group: 'contract', select: 'b.contract_type' }, { key: 'contractValidFrom', label: 'Contract valid from', type: 'date', group: 'contract', select: `to_char(b.contract_valid_from, 'YYYY-MM-DD')` }, { key: 'contractValidUntil', label: 'Contract valid until', type: 'date', group: 'contract', select: `to_char(b.contract_valid_until, 'YYYY-MM-DD')` }, { key: 'fullyExecutedAt', label: 'Fully executed at', type: 'datetime', group: 'contract', select: `to_char(b.fully_executed_at, 'YYYY-MM-DD HH24:MI')` }, // ---- First / last mile --------------------------------------------------- { key: 'firstMileAddress', label: 'Pickup address', type: 'string', group: 'firstMile', select: 'b.first_mile_pickup_address' }, { key: 'lastMileAddress', label: 'Delivery address', type: 'string', group: 'lastMile', select: 'b.last_mile_delivery_address' }, { key: 'customerTruckPlate', label: 'Customer truck plate', type: 'string', group: 'lastMile', select: 'b.customer_truck_plate_number' }, { key: 'customerTruckDriver', label: 'Customer truck driver', type: 'string', group: 'lastMile', select: 'b.customer_truck_driver_name' }, { key: 'exportHandoverMode', label: 'Handover mode', type: 'string', group: 'lastMile', select: 'b.export_handover_mode' }, // ---- Clearance ------------------------------------------------------------- { key: 'customsClearingEnabled', label: 'Customs clearing', type: 'boolean', group: 'clearance', select: 'b.customs_clearing_enabled' }, { key: 'customsClearingAgent', label: 'Clearing agent', type: 'string', group: 'clearance', select: 'b.customs_clearing_agent' }, { key: 'clearancePhase', label: 'Clearance phase', type: 'string', group: 'clearance', select: 'b.clearance_current_phase' }, { key: 'dutyRequired', label: 'Duty required', type: 'boolean', group: 'clearance', select: 'b.duty_required' }, { key: 'vesselArrivalDate', label: 'Vessel arrival', type: 'date', group: 'clearance', select: `to_char(b.vessel_arrival_date, 'YYYY-MM-DD')` }, { key: 'doCollectedDate', label: 'DO collected', type: 'date', group: 'clearance', select: `to_char(b.do_collected_date, 'YYYY-MM-DD')` }, { key: 'doubleHandling', label: 'Double handling', type: 'boolean', group: 'clearance', select: 'b.double_handling' }, ], filters: [ { key: 'created', label: 'Created', type: 'daterange' }, { key: 'statuses', label: 'Status', type: 'multiselect', options: STATUS_OPTIONS }, { key: 'tradeDirection', label: 'Direction', type: 'select', options: ['IMPORT', 'EXPORT', 'DOMESTIC'].map((v) => ({ value: v, label: v })), }, { key: 'freightType', label: 'Freight type', type: 'select', options: ['CONTAINER', 'BULK'].map((v) => ({ value: v, label: v })), }, { key: 'paymentStatus', label: 'Payment status', type: 'select', options: [ { value: 'PENDING', label: 'Pending' }, { value: 'PNR_GENERATED', label: 'PNR generated' }, { value: 'VERIFICATION_IN_PROGRESS', label: 'Verification in progress' }, { value: 'PAID', label: 'Paid' }, { value: 'FAILED', label: 'Failed' }, ] }, { key: 'companyId', label: 'Customer', type: 'text' }, { key: 'search', label: 'Search reference or customer', type: 'text' }, ], defaultSort: { key: 'createdAt', dir: 'DESC' }, scope(ctx, qb) { const { params, directions } = ctx; // andWhere, not where: `where()` resets any condition already on the // builder, so scope() would silently drop anything a caller added first. qb.andWhere('b.deleted_at IS NULL'); if (params.createdFrom) qb.andWhere('b.created_at >= :createdFrom', { createdFrom: params.createdFrom }); if (params.createdTo) qb.andWhere('b.created_at < :createdTo', { createdTo: params.createdTo }); const statuses = params.statuses as string[] | null; if (statuses?.length) qb.andWhere('b.status IN (:...statuses)', { statuses }); if (params.tradeDirection) qb.andWhere('b.trade_direction = :tradeDirection', { tradeDirection: params.tradeDirection }); if (params.freightType) qb.andWhere('b.freight_type = :freightType', { freightType: params.freightType }); if (params.paymentStatus) qb.andWhere('b.payment_status = :paymentStatus', { paymentStatus: params.paymentStatus }); if (params.companyId) qb.andWhere('b.company_id = :companyId', { companyId: params.companyId }); if (params.search) { qb.andWhere('(b.reference ILIKE :search OR c.name ILIKE :search)', { search: `%${params.search as string}%` }); } // Trade-direction ACL. Without this the export returns rows the user's own // list page would not show them. applyDirectionScope(qb, 'b.trade_direction', directions); }, };