mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
SELECT BOX
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
// No-op: dropping a column other code now depends on would reintroduce the
|
||||
// drift. The original SeparateVehicleAvailability migration owns the column.
|
||||
}
|
||||
}
|
||||
94
apps/edr-freight-api/src/seed/edr-truck-fleet.seeder.ts
Normal file
94
apps/edr-freight-api/src/seed/edr-truck-fleet.seeder.ts
Normal file
@@ -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<readonly [string, string]> = [
|
||||
['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<number>();
|
||||
|
||||
@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<void> {
|
||||
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}).`);
|
||||
}
|
||||
}
|
||||
@@ -1550,6 +1550,18 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
const [trainPickerOpen, setTrainPickerOpen] = useState(false);
|
||||
const [expandedRow, setExpandedRow] = useState<string | null>(null);
|
||||
const [targetScheduleId, setTargetScheduleId] = useState<string | null>(null);
|
||||
const [selected, setSelected] = useState<Set<string>>(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?:
|
||||
<Stack gap="sm" mt="sm">
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">
|
||||
<b>{rows.length}</b> item{rows.length !== 1 ? 's' : ''} ready to load
|
||||
{selected.size > 0 ? (
|
||||
<><b>{selected.size}</b> of {rows.length} selected</>
|
||||
) : (
|
||||
<><b>{rows.length}</b> item{rows.length !== 1 ? 's' : ''} ready to load</>
|
||||
)}
|
||||
</Text>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
@@ -1608,7 +1637,7 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
disabled={rows.length === 0}
|
||||
onClick={() => setTrainPickerOpen(true)}
|
||||
>
|
||||
Auto Load Ready Items
|
||||
{selected.size > 0 ? `Load Selected (${selected.size})` : 'Auto Load Ready Items'}
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
@@ -1670,6 +1699,14 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
<Table highlightOnHover verticalSpacing="xs" striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th w={40}>
|
||||
<Checkbox
|
||||
aria-label="Select all"
|
||||
checked={allSelected}
|
||||
indeterminate={someSelected}
|
||||
onChange={toggleAll}
|
||||
/>
|
||||
</Table.Th>
|
||||
<Table.Th w={34} />
|
||||
<Table.Th>Booking Ref</Table.Th>
|
||||
<Table.Th>GRN</Table.Th>
|
||||
@@ -1686,6 +1723,13 @@ function ReadyToLoadTab({ enabled, onChanged }: { enabled: boolean; onChanged?:
|
||||
{rows.map((r: ReadyToLoadRow) => (
|
||||
<Fragment key={r.id}>
|
||||
<Table.Tr>
|
||||
<Table.Td>
|
||||
<Checkbox
|
||||
aria-label={`Select ${r.bookingReference ?? r.id}`}
|
||||
checked={selected.has(r.id)}
|
||||
onChange={() => toggleOne(r.id)}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
|
||||
Reference in New Issue
Block a user