From da44752c70f518fcb8afaa9e1f61e9ad3cf5d0a4 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Sat, 11 Jul 2026 08:18:04 +0000 Subject: [PATCH] SELECT BOX --- apps/edr-freight-api/src/app.module.ts | 4 + ...0000000-RepairVehicleAvailabilityColumn.ts | 32 +++++++ .../src/seed/edr-truck-fleet.seeder.ts | 94 +++++++++++++++++++ .../warehouses/ReceiveInventoryModal.tsx | 56 +++++++++-- 4 files changed, 180 insertions(+), 6 deletions(-) create mode 100644 apps/edr-freight-api/src/migrations/2110000000000-RepairVehicleAvailabilityColumn.ts create mode 100644 apps/edr-freight-api/src/seed/edr-truck-fleet.seeder.ts diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index 224a75ada..2bee84923 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -69,6 +69,7 @@ import { MarshallingDemoTrainsSeeder } from "./seed/marshalling-demo-trains.seed import { FreightPermissionKeyMigrationSeeder } from "./seed/freight-permission-key-migration.seeder"; import { DemoFreightDataSeeder } from "./seed/demo-freight-data.seeder"; import { GovCompaniesSeeder } from "./seed/gov-companies.seeder"; +import { EdrTruckFleetSeeder } from "./seed/edr-truck-fleet.seeder"; import { ApprovedFirstLastMileDemoBookingsSeeder } from "./seed/approved-first-lastmile-demo-bookings.seeder"; import { PaidImportExportMileDemoSeeder } from "./seed/paid-import-export-mile-demo.seeder"; //New Trains, Wagons, Container and Cargo management modules @@ -201,6 +202,7 @@ import { LoggerMiddleware } from "./logger.middleware"; FreightPermissionKeyMigrationSeeder, DemoFreightDataSeeder, GovCompaniesSeeder, + EdrTruckFleetSeeder, IndodeFacilitySeeder, Batch14TestDataSeeder, Batch5TestDataSeeder, @@ -233,6 +235,7 @@ export class AppModule implements OnApplicationBootstrap { private readonly freightPermissionKeyMigrationSeeder: FreightPermissionKeyMigrationSeeder, private readonly demoFreightDataSeeder: DemoFreightDataSeeder, private readonly govCompaniesSeeder: GovCompaniesSeeder, + private readonly edrTruckFleetSeeder: EdrTruckFleetSeeder, ) { } async onApplicationBootstrap() { @@ -263,6 +266,7 @@ export class AppModule implements OnApplicationBootstrap { // Government entities (with importer/exporter profiles) that government // bookings bill to. Idempotent — keyed by fixed IDs. await this.govCompaniesSeeder.run(); + await this.edrTruckFleetSeeder.run(); } configure(consumer: MiddlewareConsumer) { diff --git a/apps/edr-freight-api/src/migrations/2110000000000-RepairVehicleAvailabilityColumn.ts b/apps/edr-freight-api/src/migrations/2110000000000-RepairVehicleAvailabilityColumn.ts new file mode 100644 index 000000000..c68a6c720 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/2110000000000-RepairVehicleAvailabilityColumn.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +/** + * Repair migration. `SeparateVehicleAvailability1890000000000` is recorded in + * public.migrations but the `availability` column is absent on some databases + * (recorded-but-not-applied drift). Because the original is already recorded, + * TypeORM will not re-run it, so `vehiclesService.findAll` (a query builder that + * selects every entity column) 500s with `column "availability" does not exist`. + * + * This re-adds the column idempotently and backfills. Safe to run everywhere: + * `IF NOT EXISTS` makes it a no-op where the column already exists. + */ +export class RepairVehicleAvailabilityColumn2110000000000 + implements MigrationInterface +{ + name = "RepairVehicleAvailabilityColumn2110000000000"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.vehicles + ADD COLUMN IF NOT EXISTS availability varchar DEFAULT 'FREE' + `); + await queryRunner.query(` + UPDATE freight.vehicles SET availability = 'FREE' WHERE availability IS NULL + `); + } + + public async down(): Promise { + // No-op: dropping a column other code now depends on would reintroduce the + // drift. The original SeparateVehicleAvailability migration owns the column. + } +} diff --git a/apps/edr-freight-api/src/seed/edr-truck-fleet.seeder.ts b/apps/edr-freight-api/src/seed/edr-truck-fleet.seeder.ts new file mode 100644 index 000000000..e8349c806 --- /dev/null +++ b/apps/edr-freight-api/src/seed/edr-truck-fleet.seeder.ts @@ -0,0 +1,94 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { DataSource } from 'typeorm'; + +/** + * EDR-owned truck fleet used by first-mile / last-mile pickup & delivery. + * These 62 trucks used to be a hardcoded list in the truck-arrival UI; the + * first/last-mile flows now read the fleet from `freight.vehicles` via + * VehiclesService, so the fleet must exist as vehicle rows. + * + * `[powerPlate, trailerPlate]` in fleet order 1..62. Region code "03-ET" is + * shared by every truck. NB: 56 trucks are configured for 40ft containers, 6 + * for 20ft only — the specific 6 are not yet confirmed, so all default to 40ft. + * List `TWENTY_FT_SEQS` when known. + */ +const EDR_TRUCK_FLEET: ReadonlyArray = [ + ['A45843', '43495'], ['A45866', '43470'], ['A45853', '43508'], ['A45849', '43414'], + ['A45845', '43492'], ['A45820', '43487'], ['A45842', '43478'], ['A45841', '43515'], + ['A45832', '43504'], ['A45856', '43510'], ['A45865', '43490'], ['A45855', '43485'], + ['A45840', '43499'], ['A45867', '43493'], ['A45833', '43466'], ['A45858', '43496'], + ['A45819', '43474'], ['A45834', '43502'], ['A45868', '43469'], ['A45831', '43488'], + ['A45828', '43479'], ['A45850', '43505'], ['A45823', '43480'], ['A45838', '43472'], + ['A45854', '43500'], ['A45839', '43486'], ['A45861', '43513'], ['A45830', '43501'], + ['A45826', '43498'], ['A45836', '43467'], ['A45822', '43512'], ['A45821', '43210'], + ['A45837', '43475'], ['A45860', '43497'], ['A45863', '43477'], ['A45825', '43483'], + ['A45829', '43473'], ['A45824', '43491'], ['A45857', '43481'], ['A45851', '43509'], + ['A45827', '43468'], ['A45859', '43887'], ['A45846', '43471'], ['A45847', '43511'], + ['A45852', '43484'], ['A45844', '43476'], ['A45835', '43482'], ['A45864', '43503'], + ['A45848', '43494'], ['A45862', '43465'], ['A39105', '41218'], ['A39098', '41220'], + ['A29900', '41865'], ['A39097', '41226'], ['A39104', '41225'], ['A39103', '41223'], + ['A39106', '41221'], ['A39107', '41215'], ['A39094', '41222'], ['A39099', '41216'], + ['A39092', '41224'], ['A31801', '41214'], +]; + +/** Fleet sequence numbers (1-based) that are 20ft-only. 6 of 62 — fill once confirmed. */ +const TWENTY_FT_SEQS = new Set(); + +@Injectable() +export class EdrTruckFleetSeeder { + private readonly logger = new Logger(EdrTruckFleetSeeder.name); + + constructor(private readonly dataSource: DataSource) {} + + /** + * Idempotent: `ON CONFLICT (plate_number) DO NOTHING`. Uses raw SQL with an + * explicit column list on purpose — the `Vehicle` entity declares an + * `availability` column that does not exist in the DB (schema drift), so a + * repository insert would fail. This inserts only real columns. + */ + async run(): Promise { + const columns = [ + 'code', 'plate_number', 'registration_number', 'power_plate_no', 'trailer_plate_no', + 'vehicle_type', 'manufacturer', 'model', 'year', 'fuel_type', 'capacity', + 'status', 'ownership', 'currency', 'description', + ]; + + const rows: unknown[][] = EDR_TRUCK_FLEET.map(([power, trailer], i) => { + const seq = i + 1; + const ft = TWENTY_FT_SEQS.has(seq) ? '20ft' : '40ft'; + return [ + `EDR-TRK-${String(seq).padStart(3, '0')}`, + `03-ET ${power}`, + trailer, + `03-ET ${power}`, + trailer, + 'TRUCK', + 'EDR', + `${ft} Container Truck`, + 2018, + 'DIESEL', + TWENTY_FT_SEQS.has(seq) ? 1 : 2, + 'ACTIVE', + 'EDR', + 'ETB', + `EDR-owned container truck configured for ${ft} containers.`, + ]; + }); + + const params: unknown[] = []; + const valueGroups = rows.map((row, r) => { + const placeholders = row.map((_, c) => `$${r * columns.length + c + 1}`); + params.push(...row); + return `(${placeholders.join(', ')})`; + }); + + const result = await this.dataSource.query( + `INSERT INTO freight.vehicles (${columns.join(', ')}) VALUES ${valueGroups.join(', ')} ` + + `ON CONFLICT (plate_number) DO NOTHING`, + params, + ); + + const inserted = Array.isArray(result) ? result.length : (result?.affectedRows ?? 0); + this.logger.log(`EDR truck fleet seed: ${EDR_TRUCK_FLEET.length} trucks ensured (new: ${inserted}).`); + } +} diff --git a/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx b/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx index 55722f04a..93c3bf64e 100644 --- a/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/warehouses/ReceiveInventoryModal.tsx @@ -1550,6 +1550,18 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: const [trainPickerOpen, setTrainPickerOpen] = useState(false); const [expandedRow, setExpandedRow] = useState(null); const [targetScheduleId, setTargetScheduleId] = useState(null); + const [selected, setSelected] = useState>(new Set()); + + const allSelected = rows.length > 0 && selected.size === rows.length; + const someSelected = selected.size > 0 && !allSelected; + const toggleAll = () => setSelected(allSelected ? new Set() : new Set(rows.map((r) => r.id))); + const toggleOne = (id: string) => + setSelected((prev) => { + const next = new Set(prev); + next.has(id) ? next.delete(id) : next.add(id); + return next; + }); + // Loading is always onto a SPECIFIC pre-dispatch train. No train -> no auto-load. const { data: trains = [], isLoading: trainsLoading } = useQuery({ queryKey: ['warehouse-inventory', 'loadable-trains'], @@ -1557,11 +1569,20 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: enabled: enabled && trainPickerOpen, }); const loadOntoTrain = useMutation({ - mutationFn: async (scheduleId: string) => { + mutationFn: async ({ scheduleId, onlyIds }: { scheduleId: string; onlyIds: string[] }) => { const items = await warehouseService.getTrainLoadableItems(scheduleId); - const loadableIds = items.filter((i) => i.loadable).map((i) => i.id); + let loadableIds = items.filter((i) => i.loadable).map((i) => i.id); + // When rows are checked, load only those; otherwise load every loadable item. + if (onlyIds.length) { + const picked = new Set(onlyIds); + loadableIds = loadableIds.filter((id) => picked.has(id)); + } if (!loadableIds.length) { - throw new Error('No ready items with an allocated wagon on this train'); + throw new Error( + onlyIds.length + ? 'None of the selected items have an allocated wagon on this train' + : 'No ready items with an allocated wagon on this train', + ); } return warehouseService.loadItemsOntoTrain(scheduleId, loadableIds); }, @@ -1578,7 +1599,10 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: return; } try { - const r = await loadOntoTrain.mutateAsync(targetScheduleId); + const r = await loadOntoTrain.mutateAsync({ + scheduleId: targetScheduleId, + onlyIds: [...selected], + }); const train = trains.find((t) => t.scheduleId === targetScheduleId); toast({ title: `${r.loadedCount} item(s) loaded onto train ${train?.trainNumber ?? ''}`.trim(), @@ -1588,6 +1612,7 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: }); setTrainPickerOpen(false); setTargetScheduleId(null); + setSelected(new Set()); onChanged?.(); } catch (error) { toast({ variant: 'destructive', title: 'Load failed', description: extractErrorMessage(error) }); @@ -1598,7 +1623,11 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: - {rows.length} item{rows.length !== 1 ? 's' : ''} ready to load + {selected.size > 0 ? ( + <>{selected.size} of {rows.length} selected + ) : ( + <>{rows.length} item{rows.length !== 1 ? 's' : ''} ready to load + )} @@ -1670,6 +1699,14 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: + + + Booking Ref GRN @@ -1686,6 +1723,13 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?: {rows.map((r: ReadyToLoadRow) => ( + + toggleOne(r.id)} + /> +