mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
interchange document generation and acknowledgement
This commit is contained in:
@@ -41,6 +41,7 @@ DEFAULT_PASSWORD=password@tria
|
|||||||
# Freight org + staff (bookings / rule-engine IAM)
|
# Freight org + staff (bookings / rule-engine IAM)
|
||||||
SEED_EDR_ORG=true
|
SEED_EDR_ORG=true
|
||||||
SEED_FREIGHT_STAFF=true
|
SEED_FREIGHT_STAFF=true
|
||||||
|
SEED_EXPORT_DJIBOUTI_INTERCHANGE_DEMO=false
|
||||||
|
|
||||||
# MinIO (used by @tria-plc/iamapi-common for file storage)
|
# MinIO (used by @tria-plc/iamapi-common for file storage)
|
||||||
MINIO_ENDPOINT=localhost
|
MINIO_ENDPOINT=localhost
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import { Batch5TestDataSeeder } from "./seed/batch5-test-data.seeder";
|
|||||||
import { Batch7TestDataSeeder } from "./seed/batch7-test-data.seeder";
|
import { Batch7TestDataSeeder } from "./seed/batch7-test-data.seeder";
|
||||||
import { Batch8TestDataSeeder } from "./seed/batch8-test-data.seeder";
|
import { Batch8TestDataSeeder } from "./seed/batch8-test-data.seeder";
|
||||||
import { WarehouseDemoSeeder } from "./seed/warehouse-demo.seeder";
|
import { WarehouseDemoSeeder } from "./seed/warehouse-demo.seeder";
|
||||||
|
import { ExportDjiboutiInterchangeDemoSeeder } from "./seed/export-djibouti-interchange-demo.seeder";
|
||||||
import { FreightPermissionKeyMigrationSeeder } from "./seed/freight-permission-key-migration.seeder";
|
import { FreightPermissionKeyMigrationSeeder } from "./seed/freight-permission-key-migration.seeder";
|
||||||
import { DemoFreightDataSeeder } from "./seed/demo-freight-data.seeder";
|
import { DemoFreightDataSeeder } from "./seed/demo-freight-data.seeder";
|
||||||
//New Trains, Wagons, Container and Cargo management modules
|
//New Trains, Wagons, Container and Cargo management modules
|
||||||
@@ -145,6 +146,7 @@ import { InterchangeDocumentsModule } from './modules/interchange-documents/inte
|
|||||||
Batch7TestDataSeeder,
|
Batch7TestDataSeeder,
|
||||||
Batch8TestDataSeeder,
|
Batch8TestDataSeeder,
|
||||||
WarehouseDemoSeeder,
|
WarehouseDemoSeeder,
|
||||||
|
ExportDjiboutiInterchangeDemoSeeder,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements OnApplicationBootstrap {
|
export class AppModule implements OnApplicationBootstrap {
|
||||||
@@ -161,6 +163,7 @@ export class AppModule implements OnApplicationBootstrap {
|
|||||||
private readonly batch7TestDataSeeder: Batch7TestDataSeeder,
|
private readonly batch7TestDataSeeder: Batch7TestDataSeeder,
|
||||||
private readonly batch8TestDataSeeder: Batch8TestDataSeeder,
|
private readonly batch8TestDataSeeder: Batch8TestDataSeeder,
|
||||||
private readonly warehouseDemoSeeder: WarehouseDemoSeeder,
|
private readonly warehouseDemoSeeder: WarehouseDemoSeeder,
|
||||||
|
private readonly exportDjiboutiInterchangeDemoSeeder: ExportDjiboutiInterchangeDemoSeeder,
|
||||||
private readonly freightPermissionKeyMigrationSeeder: FreightPermissionKeyMigrationSeeder,
|
private readonly freightPermissionKeyMigrationSeeder: FreightPermissionKeyMigrationSeeder,
|
||||||
private readonly demoFreightDataSeeder: DemoFreightDataSeeder,
|
private readonly demoFreightDataSeeder: DemoFreightDataSeeder,
|
||||||
) { }
|
) { }
|
||||||
@@ -179,6 +182,7 @@ export class AppModule implements OnApplicationBootstrap {
|
|||||||
await this.batch7TestDataSeeder.run();
|
await this.batch7TestDataSeeder.run();
|
||||||
await this.batch8TestDataSeeder.run();
|
await this.batch8TestDataSeeder.run();
|
||||||
await this.warehouseDemoSeeder.run();
|
await this.warehouseDemoSeeder.run();
|
||||||
|
await this.exportDjiboutiInterchangeDemoSeeder.run();
|
||||||
// Idempotent demo data: ≥100 wagons/type, approval chains, 4 staff users.
|
// Idempotent demo data: ≥100 wagons/type, approval chains, 4 staff users.
|
||||||
// Each block self-guards on an empty-table check, so this is safe every boot.
|
// Each block self-guards on an empty-table check, so this is safe every boot.
|
||||||
// Demo data seeds (DemoBookingsSeeder, PricingDataSeeder,
|
// Demo data seeds (DemoBookingsSeeder, PricingDataSeeder,
|
||||||
|
|||||||
@@ -265,12 +265,12 @@ export class InterchangeDocumentsService {
|
|||||||
)
|
)
|
||||||
SELECT a.booking_id AS "bookingId",
|
SELECT a.booking_id AS "bookingId",
|
||||||
a.reference AS "bookingReference",
|
a.reference AS "bookingReference",
|
||||||
'CONTAINER' AS "itemType",
|
'CONTAINER'::varchar AS "itemType",
|
||||||
COALESCE(c.booking_container_id, bc.id) AS "bookingContainerId",
|
COALESCE(c.booking_container_id, bc.id) AS "bookingContainerId",
|
||||||
NULL AS "bookingCargoId",
|
NULL::uuid AS "bookingCargoId",
|
||||||
COALESCE(c.container_number, bc.container_number) AS "containerNumber",
|
COALESCE(c.container_number, bc.container_number) AS "containerNumber",
|
||||||
c.seal_number AS "sealNumber",
|
c.seal_number AS "sealNumber",
|
||||||
NULL AS "cargoId",
|
NULL::uuid AS "cargoId",
|
||||||
a.booking_cargo_type AS "cargoType",
|
a.booking_cargo_type AS "cargoType",
|
||||||
a.cargo_free_text AS "cargoDescription",
|
a.cargo_free_text AS "cargoDescription",
|
||||||
COALESCE(bc.total_vgm_tons, c.max_gross_weight, a.cargo_total_weight_vgm) AS "weight",
|
COALESCE(bc.total_vgm_tons, c.max_gross_weight, a.cargo_total_weight_vgm) AS "weight",
|
||||||
@@ -288,12 +288,12 @@ export class InterchangeDocumentsService {
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT a.booking_id AS "bookingId",
|
SELECT a.booking_id AS "bookingId",
|
||||||
a.reference AS "bookingReference",
|
a.reference AS "bookingReference",
|
||||||
'CONTAINER' AS "itemType",
|
'CONTAINER'::varchar AS "itemType",
|
||||||
bc.id AS "bookingContainerId",
|
bc.id AS "bookingContainerId",
|
||||||
NULL AS "bookingCargoId",
|
NULL::uuid AS "bookingCargoId",
|
||||||
bc.container_number AS "containerNumber",
|
bc.container_number AS "containerNumber",
|
||||||
NULL AS "sealNumber",
|
NULL::varchar AS "sealNumber",
|
||||||
NULL AS "cargoId",
|
NULL::uuid AS "cargoId",
|
||||||
a.booking_cargo_type AS "cargoType",
|
a.booking_cargo_type AS "cargoType",
|
||||||
a.cargo_free_text AS "cargoDescription",
|
a.cargo_free_text AS "cargoDescription",
|
||||||
COALESCE(bc.total_vgm_tons, a.cargo_total_weight_vgm) AS "weight",
|
COALESCE(bc.total_vgm_tons, a.cargo_total_weight_vgm) AS "weight",
|
||||||
@@ -314,11 +314,11 @@ export class InterchangeDocumentsService {
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT a.booking_id AS "bookingId",
|
SELECT a.booking_id AS "bookingId",
|
||||||
a.reference AS "bookingReference",
|
a.reference AS "bookingReference",
|
||||||
'CARGO' AS "itemType",
|
'CARGO'::varchar AS "itemType",
|
||||||
NULL AS "bookingContainerId",
|
NULL::uuid AS "bookingContainerId",
|
||||||
cg.id AS "bookingCargoId",
|
cg.id AS "bookingCargoId",
|
||||||
NULL AS "containerNumber",
|
NULL::varchar AS "containerNumber",
|
||||||
NULL AS "sealNumber",
|
NULL::varchar AS "sealNumber",
|
||||||
cg.id AS "cargoId",
|
cg.id AS "cargoId",
|
||||||
COALESCE(cgt.cargo_type_name, a.booking_cargo_type) AS "cargoType",
|
COALESCE(cgt.cargo_type_name, a.booking_cargo_type) AS "cargoType",
|
||||||
COALESCE(cg.description, a.cargo_free_text) AS "cargoDescription",
|
COALESCE(cg.description, a.cargo_free_text) AS "cargoDescription",
|
||||||
@@ -337,12 +337,12 @@ export class InterchangeDocumentsService {
|
|||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT a.booking_id AS "bookingId",
|
SELECT a.booking_id AS "bookingId",
|
||||||
a.reference AS "bookingReference",
|
a.reference AS "bookingReference",
|
||||||
CASE WHEN a.booking_cargo_type ILIKE '%container%' THEN 'CONTAINER' ELSE 'CARGO' END AS "itemType",
|
(CASE WHEN a.booking_cargo_type ILIKE '%container%' THEN 'CONTAINER' ELSE 'CARGO' END)::varchar AS "itemType",
|
||||||
NULL AS "bookingContainerId",
|
NULL::uuid AS "bookingContainerId",
|
||||||
NULL AS "bookingCargoId",
|
NULL::uuid AS "bookingCargoId",
|
||||||
NULL AS "containerNumber",
|
NULL::varchar AS "containerNumber",
|
||||||
NULL AS "sealNumber",
|
NULL::varchar AS "sealNumber",
|
||||||
NULL AS "cargoId",
|
NULL::uuid AS "cargoId",
|
||||||
a.booking_cargo_type AS "cargoType",
|
a.booking_cargo_type AS "cargoType",
|
||||||
a.cargo_free_text AS "cargoDescription",
|
a.cargo_free_text AS "cargoDescription",
|
||||||
a.cargo_total_weight_vgm AS "weight",
|
a.cargo_total_weight_vgm AS "weight",
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ export class SchedulingReadFacade {
|
|||||||
}
|
}
|
||||||
if (filter.destination) {
|
if (filter.destination) {
|
||||||
params.push(`%${filter.destination}%`);
|
params.push(`%${filter.destination}%`);
|
||||||
where.push(`(dy.code ILIKE $${params.length} OR dy.name ILIKE $${params.length})`);
|
where.push(`(dy.code ILIKE $${params.length} OR dy.label ILIKE $${params.length})`);
|
||||||
}
|
}
|
||||||
if (filter.dateFrom) {
|
if (filter.dateFrom) {
|
||||||
params.push(filter.dateFrom);
|
params.push(filter.dateFrom);
|
||||||
@@ -351,7 +351,7 @@ export class SchedulingReadFacade {
|
|||||||
ts.train_number AS "trainNumber",
|
ts.train_number AS "trainNumber",
|
||||||
oy.code AS "origin",
|
oy.code AS "origin",
|
||||||
dy.code AS "destination",
|
dy.code AS "destination",
|
||||||
dy.name AS "destinationName",
|
dy.label AS "destinationName",
|
||||||
oy.country AS "originCountry",
|
oy.country AS "originCountry",
|
||||||
dy.country AS "destinationCountry",
|
dy.country AS "destinationCountry",
|
||||||
ts.scheduled_departure_date AS "departureTime",
|
ts.scheduled_departure_date AS "departureTime",
|
||||||
|
|||||||
@@ -1261,7 +1261,7 @@ export class WarehouseInventoryService {
|
|||||||
oy.country AS "originCountry",
|
oy.country AS "originCountry",
|
||||||
dy.country AS "destinationCountry",
|
dy.country AS "destinationCountry",
|
||||||
dy.code AS "destinationCode",
|
dy.code AS "destinationCode",
|
||||||
dy.name AS "destinationName"
|
dy.label AS "destinationName"
|
||||||
FROM freight.train_schedules ts
|
FROM freight.train_schedules ts
|
||||||
LEFT JOIN freight.yards oy ON oy.id = ts.origin_station_id
|
LEFT JOIN freight.yards oy ON oy.id = ts.origin_station_id
|
||||||
LEFT JOIN freight.yards dy ON dy.id = ts.destination_station_id
|
LEFT JOIN freight.yards dy ON dy.id = ts.destination_station_id
|
||||||
@@ -1402,6 +1402,20 @@ export class WarehouseInventoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentStatus = item.inventoryStatus ?? item.bookingStatus;
|
const currentStatus = item.inventoryStatus ?? item.bookingStatus;
|
||||||
|
if (currentStatus === 'UNLOADED_AT_DJIBOUTI_PORT') {
|
||||||
|
seenInventory.add(item.inventoryId);
|
||||||
|
result.unloadedCount += 1;
|
||||||
|
result.results.push({
|
||||||
|
bookingId: item.bookingId,
|
||||||
|
itemType: item.itemType,
|
||||||
|
itemId: item.itemId,
|
||||||
|
inventoryId: item.inventoryId,
|
||||||
|
containerNumber: item.containerNumber,
|
||||||
|
status: 'UNLOADED_AT_DJIBOUTI_PORT',
|
||||||
|
message: 'Already unloaded at Djibouti Port',
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!currentStatus || !this.EXPORT_DJIBOUTI_UNLOAD_ELIGIBLE_STATUSES.includes(currentStatus)) {
|
if (!currentStatus || !this.EXPORT_DJIBOUTI_UNLOAD_ELIGIBLE_STATUSES.includes(currentStatus)) {
|
||||||
skip(`Status ${currentStatus ?? 'UNKNOWN'} is not eligible for Djibouti export unloading`);
|
skip(`Status ${currentStatus ?? 'UNKNOWN'} is not eligible for Djibouti export unloading`);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -21,8 +21,18 @@ import { WarehouseYard } from '../modules/warehouses/entities/warehouse-yard.ent
|
|||||||
import { WarehouseZone } from '../modules/warehouses/entities/warehouse-zone.entity';
|
import { WarehouseZone } from '../modules/warehouses/entities/warehouse-zone.entity';
|
||||||
import { Warehouse } from '../modules/warehouses/entities/warehouse.entity';
|
import { Warehouse } from '../modules/warehouses/entities/warehouse.entity';
|
||||||
|
|
||||||
const TRAIN_NUMBER = 'ICD-DEMO-EXP-DJ-01';
|
const DEMO_TRAINS = [
|
||||||
const BOOKING_REFS = ['ICD-DEMO-EXP-001', 'ICD-DEMO-EXP-002', 'ICD-DEMO-EXP-003'];
|
{
|
||||||
|
trainNumber: 'ICD-DEMO-EXP-DJ-01',
|
||||||
|
bookingRefs: ['ICD-DEMO-EXP-001', 'ICD-DEMO-EXP-002', 'ICD-DEMO-EXP-003'],
|
||||||
|
arrivalOffsetHours: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trainNumber: 'ICD-DEMO-EXP-DJ-02',
|
||||||
|
bookingRefs: ['ICD-DEMO-EXP-004', 'ICD-DEMO-EXP-005', 'ICD-DEMO-EXP-006'],
|
||||||
|
arrivalOffsetHours: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const app = await NestFactory.createApplicationContext(AppModule, {
|
const app = await NestFactory.createApplicationContext(AppModule, {
|
||||||
@@ -44,13 +54,6 @@ async function main() {
|
|||||||
const scheduleRepo = dataSource.getRepository(TrainSchedule);
|
const scheduleRepo = dataSource.getRepository(TrainSchedule);
|
||||||
const scheduleBookingRepo = dataSource.getRepository(TrainScheduleBooking);
|
const scheduleBookingRepo = dataSource.getRepository(TrainScheduleBooking);
|
||||||
|
|
||||||
const existingSchedule = await scheduleRepo.findOne({ where: { trainNumber: TRAIN_NUMBER } });
|
|
||||||
if (existingSchedule) {
|
|
||||||
console.log(`Export Djibouti interchange demo already seeded: ${TRAIN_NUMBER}`);
|
|
||||||
console.log(`Schedule ID: ${existingSchedule.id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originYard =
|
const originYard =
|
||||||
(await yardRepo.findOne({ where: { code: 'MOJO' } })) ??
|
(await yardRepo.findOne({ where: { code: 'MOJO' } })) ??
|
||||||
(await yardRepo.findOne({ where: { country: 'Ethiopia' } }));
|
(await yardRepo.findOne({ where: { country: 'Ethiopia' } }));
|
||||||
@@ -83,10 +86,6 @@ async function main() {
|
|||||||
throw new Error(`Cannot seed export Djibouti interchange demo, missing: ${missing.join(', ')}`);
|
throw new Error(`Cannot seed export Djibouti interchange demo, missing: ${missing.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
const departure = new Date(now - 6 * 60 * 60 * 1000);
|
|
||||||
const arrival = new Date(now - 60 * 60 * 1000);
|
|
||||||
|
|
||||||
const locomotive =
|
const locomotive =
|
||||||
(await locomotiveRepo.findOne({ where: { code: 'ICD-DEMO-LOCO' } })) ??
|
(await locomotiveRepo.findOne({ where: { code: 'ICD-DEMO-LOCO' } })) ??
|
||||||
(await locomotiveRepo.save(
|
(await locomotiveRepo.save(
|
||||||
@@ -97,83 +96,106 @@ async function main() {
|
|||||||
}),
|
}),
|
||||||
));
|
));
|
||||||
|
|
||||||
const trainSet = await trainSetRepo.save(
|
const now = Date.now();
|
||||||
trainSetRepo.create({
|
const seededSchedules: TrainSchedule[] = [];
|
||||||
locomotiveId: locomotive.id,
|
|
||||||
totalWeightTons: 700,
|
|
||||||
totalLengthMeters: 360,
|
|
||||||
wagonCount: 12,
|
|
||||||
status: 'COMPLETED',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const schedule = await scheduleRepo.save(
|
for (const [trainIndex, demo] of DEMO_TRAINS.entries()) {
|
||||||
scheduleRepo.create({
|
const existingSchedule = await scheduleRepo.findOne({ where: { trainNumber: demo.trainNumber } });
|
||||||
trainSetId: trainSet.id,
|
if (existingSchedule) {
|
||||||
originStationId: originYard!.id,
|
console.log(`Export Djibouti interchange demo already seeded: ${demo.trainNumber}`);
|
||||||
destinationStationId: destinationYard!.id,
|
console.log(`Schedule ID: ${existingSchedule.id}`);
|
||||||
scheduledDepartureDate: departure,
|
seededSchedules.push(existingSchedule);
|
||||||
scheduledArrivalDate: arrival,
|
continue;
|
||||||
actualArrivalAt: arrival,
|
}
|
||||||
status: 'ARRIVED' as TrainSchedule['status'],
|
|
||||||
trainNumber: TRAIN_NUMBER,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const [index, reference] of BOOKING_REFS.entries()) {
|
const arrival = new Date(now - demo.arrivalOffsetHours * 60 * 60 * 1000);
|
||||||
const weight = 5200 + index * 800;
|
const departure = new Date(arrival.getTime() - 5 * 60 * 60 * 1000);
|
||||||
const booking = await bookingRepo.save(
|
|
||||||
bookingRepo.create({
|
const trainSet = await trainSetRepo.save(
|
||||||
reference,
|
trainSetRepo.create({
|
||||||
originYardId: originYard!.id,
|
locomotiveId: locomotive.id,
|
||||||
destinationYardId: destinationYard!.id,
|
totalWeightTons: 700 + trainIndex * 80,
|
||||||
serviceTypeId: serviceType!.id,
|
totalLengthMeters: 360 + trainIndex * 20,
|
||||||
status: 'IN_TRANSIT',
|
wagonCount: 12 + trainIndex,
|
||||||
paymentStatus: 'PAID',
|
status: 'COMPLETED',
|
||||||
scheduledDate: new Date(),
|
|
||||||
contractType: 'SPOT',
|
|
||||||
equipmentReturn: 'TERMINAL',
|
|
||||||
paymentCurrency: 'ETB',
|
|
||||||
totalAmount: 0,
|
|
||||||
isGovernment: false,
|
|
||||||
tradeDirection: 'EXPORT',
|
|
||||||
freightType: index % 2 === 0 ? 'CONTAINER' : 'BULK',
|
|
||||||
cargoTypeId: cargoType?.id ?? null,
|
|
||||||
cargoFreeText: cargoType ? null : `Export Djibouti interchange demo cargo ${index + 1}`,
|
|
||||||
cargoTotalWeightVgm: weight,
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await inventoryRepo.save(
|
const schedule = await scheduleRepo.save(
|
||||||
inventoryRepo.create({
|
scheduleRepo.create({
|
||||||
warehouseId: warehouse!.id,
|
trainSetId: trainSet.id,
|
||||||
yardId: warehouseYard!.id,
|
originStationId: originYard!.id,
|
||||||
zoneId: warehouseZone!.id,
|
destinationStationId: destinationYard!.id,
|
||||||
bookingId: booking.id,
|
scheduledDepartureDate: departure,
|
||||||
quantity: 1,
|
scheduledArrivalDate: arrival,
|
||||||
weight,
|
actualArrivalAt: arrival,
|
||||||
status: 'DISPATCHED',
|
status: 'ARRIVED' as TrainSchedule['status'],
|
||||||
inspectionStatus: 'PASSED',
|
trainNumber: demo.trainNumber,
|
||||||
arrivedAt: new Date(now - 4 * 60 * 60 * 1000),
|
direction: 'EXPORT',
|
||||||
inspectedAt: new Date(now - 3 * 60 * 60 * 1000),
|
|
||||||
readyForLoadingAt: new Date(now - 2 * 60 * 60 * 1000),
|
|
||||||
loadedAt: new Date(now - 90 * 60 * 1000),
|
|
||||||
dispatchedAt: new Date(now - 70 * 60 * 1000),
|
|
||||||
notes: '[ICD-DEMO] Eligible for Djibouti export unload and interchange generation',
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await scheduleBookingRepo.save(
|
for (const [bookingIndex, reference] of demo.bookingRefs.entries()) {
|
||||||
scheduleBookingRepo.create({
|
const weight = 5200 + trainIndex * 600 + bookingIndex * 800;
|
||||||
trainScheduleId: schedule.id,
|
const booking = await bookingRepo.save(
|
||||||
bookingId: booking.id,
|
bookingRepo.create({
|
||||||
}),
|
reference,
|
||||||
);
|
originYardId: originYard!.id,
|
||||||
|
destinationYardId: destinationYard!.id,
|
||||||
|
serviceTypeId: serviceType!.id,
|
||||||
|
status: 'IN_TRANSIT',
|
||||||
|
paymentStatus: 'PAID',
|
||||||
|
scheduledDate: new Date(),
|
||||||
|
contractType: 'SPOT',
|
||||||
|
equipmentReturn: 'TERMINAL',
|
||||||
|
paymentCurrency: 'ETB',
|
||||||
|
totalAmount: 0,
|
||||||
|
isGovernment: false,
|
||||||
|
tradeDirection: 'EXPORT',
|
||||||
|
freightType: bookingIndex % 2 === 0 ? 'CONTAINER' : 'BULK',
|
||||||
|
cargoTypeId: cargoType?.id ?? null,
|
||||||
|
cargoFreeText: cargoType
|
||||||
|
? null
|
||||||
|
: `Export Djibouti interchange demo cargo ${trainIndex + 1}-${bookingIndex + 1}`,
|
||||||
|
cargoTotalWeightVgm: weight,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await inventoryRepo.save(
|
||||||
|
inventoryRepo.create({
|
||||||
|
warehouseId: warehouse!.id,
|
||||||
|
yardId: warehouseYard!.id,
|
||||||
|
zoneId: warehouseZone!.id,
|
||||||
|
bookingId: booking.id,
|
||||||
|
quantity: 1,
|
||||||
|
weight,
|
||||||
|
status: 'DISPATCHED',
|
||||||
|
inspectionStatus: 'PASSED',
|
||||||
|
arrivedAt: new Date(departure.getTime() + 2 * 60 * 60 * 1000),
|
||||||
|
inspectedAt: new Date(departure.getTime() + 3 * 60 * 60 * 1000),
|
||||||
|
readyForLoadingAt: new Date(departure.getTime() + 4 * 60 * 60 * 1000),
|
||||||
|
loadedAt: new Date(departure.getTime() + 4.5 * 60 * 60 * 1000),
|
||||||
|
dispatchedAt: departure,
|
||||||
|
notes: '[ICD-DEMO] Eligible for Djibouti export unload and interchange generation',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await scheduleBookingRepo.save(
|
||||||
|
scheduleBookingRepo.create({
|
||||||
|
trainScheduleId: schedule.id,
|
||||||
|
bookingId: booking.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
seededSchedules.push(schedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Export Djibouti interchange demo seeded.');
|
console.log('Export Djibouti interchange demo seeded.');
|
||||||
console.log(`Train number: ${TRAIN_NUMBER}`);
|
for (const schedule of seededSchedules) {
|
||||||
console.log(`Schedule ID: ${schedule.id}`);
|
console.log(`Train number: ${schedule.trainNumber}`);
|
||||||
|
console.log(`Schedule ID: ${schedule.id}`);
|
||||||
|
}
|
||||||
console.log('Open Djibouti Unloading, click "Auto Unload Export Items", then check Interchange Documents.');
|
console.log('Open Djibouti Unloading, click "Auto Unload Export Items", then check Interchange Documents.');
|
||||||
} finally {
|
} finally {
|
||||||
await app.close();
|
await app.close();
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
import { Booking } from '../modules/bookings/entities/booking.entity';
|
||||||
|
import { Locomotive } from '../modules/locomotives/entities/locomotive.entity';
|
||||||
|
import { CargoType } from '../modules/rule-engine/entities/cargo-type.entity';
|
||||||
|
import { ServiceType } from '../modules/rule-engine/entities/service-type.entity';
|
||||||
|
import { Yard } from '../modules/rule-engine/entities/yard.entity';
|
||||||
|
import { TrainSchedule } from '../modules/train-schedules/entities/train-schedule.entity';
|
||||||
|
import { TrainScheduleBooking } from '../modules/train-schedules/entities/train-schedule-booking.entity';
|
||||||
|
import { TrainSet } from '../modules/train-sets/entities/train-set.entity';
|
||||||
|
import { WarehouseInventory } from '../modules/warehouses/entities/warehouse-inventory.entity';
|
||||||
|
import { WarehouseYard } from '../modules/warehouses/entities/warehouse-yard.entity';
|
||||||
|
import { WarehouseZone } from '../modules/warehouses/entities/warehouse-zone.entity';
|
||||||
|
import { Warehouse } from '../modules/warehouses/entities/warehouse.entity';
|
||||||
|
|
||||||
|
const SEED_FLAG = 'SEED_EXPORT_DJIBOUTI_INTERCHANGE_DEMO';
|
||||||
|
|
||||||
|
const DEMO_TRAINS = [
|
||||||
|
{
|
||||||
|
trainNumber: 'ICD-DEMO-EXP-DJ-01',
|
||||||
|
bookingRefs: ['ICD-DEMO-EXP-001', 'ICD-DEMO-EXP-002', 'ICD-DEMO-EXP-003'],
|
||||||
|
arrivalOffsetHours: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trainNumber: 'ICD-DEMO-EXP-DJ-02',
|
||||||
|
bookingRefs: ['ICD-DEMO-EXP-004', 'ICD-DEMO-EXP-005', 'ICD-DEMO-EXP-006'],
|
||||||
|
arrivalOffsetHours: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ExportDjiboutiInterchangeDemoSeeder {
|
||||||
|
private readonly logger = new Logger(ExportDjiboutiInterchangeDemoSeeder.name);
|
||||||
|
|
||||||
|
constructor(private readonly dataSource: DataSource) {}
|
||||||
|
|
||||||
|
async run(): Promise<void> {
|
||||||
|
if (process.env[SEED_FLAG]?.trim().toLowerCase() !== 'true') {
|
||||||
|
this.logger.log(`Skipping export Djibouti interchange demo seed because ${SEED_FLAG} is not enabled`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const yardRepo = this.dataSource.getRepository(Yard);
|
||||||
|
const serviceTypeRepo = this.dataSource.getRepository(ServiceType);
|
||||||
|
const cargoTypeRepo = this.dataSource.getRepository(CargoType);
|
||||||
|
const warehouseRepo = this.dataSource.getRepository(Warehouse);
|
||||||
|
const warehouseYardRepo = this.dataSource.getRepository(WarehouseYard);
|
||||||
|
const warehouseZoneRepo = this.dataSource.getRepository(WarehouseZone);
|
||||||
|
const bookingRepo = this.dataSource.getRepository(Booking);
|
||||||
|
const inventoryRepo = this.dataSource.getRepository(WarehouseInventory);
|
||||||
|
const locomotiveRepo = this.dataSource.getRepository(Locomotive);
|
||||||
|
const trainSetRepo = this.dataSource.getRepository(TrainSet);
|
||||||
|
const scheduleRepo = this.dataSource.getRepository(TrainSchedule);
|
||||||
|
const scheduleBookingRepo = this.dataSource.getRepository(TrainScheduleBooking);
|
||||||
|
|
||||||
|
const originYard =
|
||||||
|
(await yardRepo.findOne({ where: { code: 'MOJO' } })) ??
|
||||||
|
(await yardRepo.findOne({ where: { country: 'Ethiopia' } }));
|
||||||
|
const destinationYard =
|
||||||
|
(await yardRepo.findOne({ where: { code: 'DJIB_PORT' } })) ??
|
||||||
|
(await yardRepo.findOne({ where: { code: 'DJIBOUTI' } })) ??
|
||||||
|
(await yardRepo.findOne({ where: { country: 'Djibouti' } }));
|
||||||
|
const serviceType =
|
||||||
|
(await serviceTypeRepo.findOne({ where: { code: 'RAIL_CONTAINER' } })) ??
|
||||||
|
(await serviceTypeRepo.findOne({ where: { isActive: true } }));
|
||||||
|
const cargoType = await cargoTypeRepo.findOne({ where: { isActive: true } });
|
||||||
|
const warehouse = await warehouseRepo.findOne({ where: { code: 'INDODE_OPEN' } });
|
||||||
|
const warehouseYard = warehouse
|
||||||
|
? await warehouseYardRepo.findOne({ where: { warehouseId: warehouse.id } })
|
||||||
|
: null;
|
||||||
|
const warehouseZone = warehouseYard
|
||||||
|
? await warehouseZoneRepo.findOne({ where: { yardId: warehouseYard.id } })
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const missing = [
|
||||||
|
!originYard ? 'Ethiopian origin yard' : '',
|
||||||
|
!destinationYard ? 'Djibouti destination yard' : '',
|
||||||
|
!serviceType ? 'service type' : '',
|
||||||
|
!warehouse ? 'INDODE_OPEN warehouse' : '',
|
||||||
|
!warehouseYard ? 'warehouse yard' : '',
|
||||||
|
!warehouseZone ? 'warehouse zone' : '',
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
if (missing.length) {
|
||||||
|
this.logger.warn(`Cannot seed export Djibouti interchange demo, missing: ${missing.join(', ')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const locomotive =
|
||||||
|
(await locomotiveRepo.findOne({ where: { code: 'ICD-DEMO-LOCO' } })) ??
|
||||||
|
(await locomotiveRepo.save(
|
||||||
|
locomotiveRepo.create({
|
||||||
|
code: 'ICD-DEMO-LOCO',
|
||||||
|
name: 'Interchange Demo Locomotive',
|
||||||
|
maxPullWeightTons: 4000,
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
let seeded = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
for (const [trainIndex, demo] of DEMO_TRAINS.entries()) {
|
||||||
|
const existingSchedule = await scheduleRepo.findOne({ where: { trainNumber: demo.trainNumber } });
|
||||||
|
if (existingSchedule) {
|
||||||
|
skipped += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrival = new Date(now - demo.arrivalOffsetHours * 60 * 60 * 1000);
|
||||||
|
const departure = new Date(arrival.getTime() - 5 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const trainSet = await trainSetRepo.save(
|
||||||
|
trainSetRepo.create({
|
||||||
|
locomotiveId: locomotive.id,
|
||||||
|
totalWeightTons: 700 + trainIndex * 80,
|
||||||
|
totalLengthMeters: 360 + trainIndex * 20,
|
||||||
|
wagonCount: 12 + trainIndex,
|
||||||
|
status: 'COMPLETED',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const schedule = await scheduleRepo.save(
|
||||||
|
scheduleRepo.create({
|
||||||
|
trainSetId: trainSet.id,
|
||||||
|
originStationId: originYard!.id,
|
||||||
|
destinationStationId: destinationYard!.id,
|
||||||
|
scheduledDepartureDate: departure,
|
||||||
|
scheduledArrivalDate: arrival,
|
||||||
|
actualArrivalAt: arrival,
|
||||||
|
status: 'ARRIVED' as TrainSchedule['status'],
|
||||||
|
trainNumber: demo.trainNumber,
|
||||||
|
direction: 'EXPORT',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [bookingIndex, reference] of demo.bookingRefs.entries()) {
|
||||||
|
const weight = 5200 + trainIndex * 600 + bookingIndex * 800;
|
||||||
|
const booking = await bookingRepo.save(
|
||||||
|
bookingRepo.create({
|
||||||
|
reference,
|
||||||
|
originYardId: originYard!.id,
|
||||||
|
destinationYardId: destinationYard!.id,
|
||||||
|
serviceTypeId: serviceType!.id,
|
||||||
|
status: 'IN_TRANSIT',
|
||||||
|
paymentStatus: 'PAID',
|
||||||
|
scheduledDate: new Date(),
|
||||||
|
contractType: 'SPOT',
|
||||||
|
equipmentReturn: 'TERMINAL',
|
||||||
|
paymentCurrency: 'ETB',
|
||||||
|
totalAmount: 0,
|
||||||
|
isGovernment: false,
|
||||||
|
tradeDirection: 'EXPORT',
|
||||||
|
freightType: bookingIndex % 2 === 0 ? 'CONTAINER' : 'BULK',
|
||||||
|
cargoTypeId: cargoType?.id ?? null,
|
||||||
|
cargoFreeText: cargoType
|
||||||
|
? null
|
||||||
|
: `Export Djibouti interchange demo cargo ${trainIndex + 1}-${bookingIndex + 1}`,
|
||||||
|
cargoTotalWeightVgm: weight,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await inventoryRepo.save(
|
||||||
|
inventoryRepo.create({
|
||||||
|
warehouseId: warehouse!.id,
|
||||||
|
yardId: warehouseYard!.id,
|
||||||
|
zoneId: warehouseZone!.id,
|
||||||
|
bookingId: booking.id,
|
||||||
|
quantity: 1,
|
||||||
|
weight,
|
||||||
|
status: 'DISPATCHED',
|
||||||
|
inspectionStatus: 'PASSED',
|
||||||
|
arrivedAt: new Date(departure.getTime() + 2 * 60 * 60 * 1000),
|
||||||
|
inspectedAt: new Date(departure.getTime() + 3 * 60 * 60 * 1000),
|
||||||
|
readyForLoadingAt: new Date(departure.getTime() + 4 * 60 * 60 * 1000),
|
||||||
|
loadedAt: new Date(departure.getTime() + 4.5 * 60 * 60 * 1000),
|
||||||
|
dispatchedAt: departure,
|
||||||
|
notes: '[ICD-DEMO] Eligible for Djibouti export unload and interchange generation',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await scheduleBookingRepo.save(
|
||||||
|
scheduleBookingRepo.create({
|
||||||
|
trainScheduleId: schedule.id,
|
||||||
|
bookingId: booking.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
seeded += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Export Djibouti interchange demo seed complete: ${seeded} train(s) seeded, ${skipped} skipped`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`ExportDjiboutiInterchangeDemoSeeder failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export const API_BASE_URL =
|
export const API_BASE_URL =
|
||||||
import.meta.env.VITE_BASE_API_URL ||
|
import.meta.env.VITE_BASE_API_URL ||
|
||||||
import.meta.env.VITE_API_URL ||
|
import.meta.env.VITE_API_URL ||
|
||||||
'https://edrfreightapi.triaplc.com';
|
'https://edrfreightapi.triaplc.com';
|
||||||
|
|
||||||
//export const API_BASE_URL = 'http://localhost:3001';
|
//export const API_BASE_URL = 'http://localhost:3001';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
Modal,
|
Modal,
|
||||||
|
Alert,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -168,7 +169,7 @@ function ExportTrainDetailRows({
|
|||||||
export default function ExportDjiboutiUnloadingQueuePage() {
|
export default function ExportDjiboutiUnloadingQueuePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { data: trains = [], isLoading } = useExportDjiboutiArrivalQueue();
|
const { data: trains = [], isLoading, isError, error } = useExportDjiboutiArrivalQueue();
|
||||||
const { data: interchangeDocuments = [] } = useInterchangeDocuments({ direction: 'EXPORT' });
|
const { data: interchangeDocuments = [] } = useInterchangeDocuments({ direction: 'EXPORT' });
|
||||||
const autoUnload = useAutoUnloadExportAtDjibouti();
|
const autoUnload = useAutoUnloadExportAtDjibouti();
|
||||||
const generateInterchange = useGenerateInterchangeDocument();
|
const generateInterchange = useGenerateInterchangeDocument();
|
||||||
@@ -272,6 +273,10 @@ export default function ExportDjiboutiUnloadingQueuePage() {
|
|||||||
<Group justify="center" py="xl">
|
<Group justify="center" py="xl">
|
||||||
<Loader />
|
<Loader />
|
||||||
</Group>
|
</Group>
|
||||||
|
) : isError ? (
|
||||||
|
<Alert color="red" variant="light" title="Could not load arrived export trains">
|
||||||
|
{getErrorMessage(error) ?? 'Check your API connection and sign in again.'}
|
||||||
|
</Alert>
|
||||||
) : trains.length === 0 ? (
|
) : trains.length === 0 ? (
|
||||||
<VisualEmptyState
|
<VisualEmptyState
|
||||||
variant="train"
|
variant="train"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { CheckCircle2, Eye, FileText, Search, XCircle } from 'lucide-react';
|
import { CheckCircle2, Download, Eye, FileText, Printer, Search, XCircle } from 'lucide-react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
import { PageContainer, PageHeader } from '@/components/page';
|
import { PageContainer, PageHeader } from '@/components/page';
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
useInterchangeDocuments,
|
useInterchangeDocuments,
|
||||||
} from '@/hooks/useInterchangeDocuments';
|
} from '@/hooks/useInterchangeDocuments';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import { interchangeDocumentsService } from '@/services/interchange-documents.service';
|
||||||
import type { InterchangeDocument, InterchangeDocumentStatus } from '@/types/interchangeDocument';
|
import type { InterchangeDocument, InterchangeDocumentStatus } from '@/types/interchangeDocument';
|
||||||
|
|
||||||
const statusColor: Record<InterchangeDocumentStatus, string> = {
|
const statusColor: Record<InterchangeDocumentStatus, string> = {
|
||||||
@@ -45,6 +46,116 @@ const getErrorMessage = (error: unknown) => {
|
|||||||
return error instanceof Error ? error.message : undefined;
|
return error instanceof Error ? error.message : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const escapeHtml = (value: unknown) =>
|
||||||
|
String(value ?? '-')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
|
const filenameFor = (document: InterchangeDocument) =>
|
||||||
|
`${document.documentNo || document.id}-interchange-document.html`.replace(/[\\/:*?"<>|]/g, '-');
|
||||||
|
|
||||||
|
const buildPrintableInterchangeHtml = (document: InterchangeDocument) => {
|
||||||
|
const items = document.items ?? [];
|
||||||
|
const rows = items
|
||||||
|
.map(
|
||||||
|
(item, index) => `
|
||||||
|
<tr>
|
||||||
|
<td>${index + 1}</td>
|
||||||
|
<td>${escapeHtml(item.bookingReference ?? item.bookingId?.slice(0, 8))}</td>
|
||||||
|
<td>${escapeHtml(item.itemType)}</td>
|
||||||
|
<td>${escapeHtml(item.containerNumber)}</td>
|
||||||
|
<td>${escapeHtml(item.sealNumber)}</td>
|
||||||
|
<td>${escapeHtml(item.cargoType ?? item.cargoDescription)}</td>
|
||||||
|
<td>${escapeHtml(formatNumber(item.weight))}</td>
|
||||||
|
<td>${escapeHtml(formatNumber(item.quantity))}</td>
|
||||||
|
<td>${escapeHtml(item.wagonNumber)}</td>
|
||||||
|
<td>${escapeHtml(item.conditionStatus)}</td>
|
||||||
|
<td>${escapeHtml(item.damageDescription)}</td>
|
||||||
|
</tr>`,
|
||||||
|
)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
return `<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>${escapeHtml(document.documentNo)} Interchange Document</title>
|
||||||
|
<style>
|
||||||
|
@page { size: A4 landscape; margin: 14mm; }
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: Arial, sans-serif; color: #111827; margin: 0; }
|
||||||
|
.top { display: flex; justify-content: space-between; gap: 24px; border-bottom: 2px solid #111827; padding-bottom: 14px; }
|
||||||
|
h1 { margin: 0; font-size: 24px; }
|
||||||
|
.muted { color: #4b5563; font-size: 12px; }
|
||||||
|
.stamp { border: 2px solid #15803d; color: #15803d; border-radius: 999px; padding: 14px 18px; text-align: center; font-weight: 700; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px 18px; margin: 18px 0; }
|
||||||
|
.field { border-bottom: 1px solid #d1d5db; padding-bottom: 6px; }
|
||||||
|
.label { color: #6b7280; font-size: 10px; text-transform: uppercase; letter-spacing: .04em; }
|
||||||
|
.value { font-size: 13px; font-weight: 700; margin-top: 3px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin-top: 12px; font-size: 11px; }
|
||||||
|
th, td { border: 1px solid #d1d5db; padding: 6px; text-align: left; vertical-align: top; }
|
||||||
|
th { background: #f3f4f6; }
|
||||||
|
.signatures { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-top: 34px; }
|
||||||
|
.signature { border-top: 1px solid #111827; padding-top: 8px; min-height: 48px; }
|
||||||
|
.footer { margin-top: 16px; font-size: 10px; color: #6b7280; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="top">
|
||||||
|
<div>
|
||||||
|
<h1>EDR / Djibouti Port Interchange Document</h1>
|
||||||
|
<div class="muted">Official export handover document</div>
|
||||||
|
<div class="muted">Document No: ${escapeHtml(document.documentNo)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stamp">${escapeHtml(document.status)}<br/>SIGNED HANDOVER</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div class="field"><div class="label">Direction</div><div class="value">${escapeHtml(document.direction)}</div></div>
|
||||||
|
<div class="field"><div class="label">Train No</div><div class="value">${escapeHtml(document.trainNo)}</div></div>
|
||||||
|
<div class="field"><div class="label">Schedule</div><div class="value">${escapeHtml(document.scheduleId)}</div></div>
|
||||||
|
<div class="field"><div class="label">Handover Location</div><div class="value">${escapeHtml(document.handoverLocation)}</div></div>
|
||||||
|
<div class="field"><div class="label">Handover From</div><div class="value">${escapeHtml(document.handoverFrom)}</div></div>
|
||||||
|
<div class="field"><div class="label">Handover To</div><div class="value">${escapeHtml(document.handoverTo)}</div></div>
|
||||||
|
<div class="field"><div class="label">Generated At</div><div class="value">${escapeHtml(formatDate(document.generatedAt))}</div></div>
|
||||||
|
<div class="field"><div class="label">Acknowledged At</div><div class="value">${escapeHtml(formatDate(document.acknowledgedAt))}</div></div>
|
||||||
|
<div class="field"><div class="label">Signed by EDR</div><div class="value">${escapeHtml(document.generatedBy)}</div></div>
|
||||||
|
<div class="field"><div class="label">Signed by Djibouti Port</div><div class="value">${escapeHtml(document.acknowledgedBy)}</div></div>
|
||||||
|
<div class="field"><div class="label">Port Operator</div><div class="value">${escapeHtml(document.portOperatorName)}</div></div>
|
||||||
|
<div class="field"><div class="label">Manifest Ref</div><div class="value">${escapeHtml(document.manifestReference)}</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Booking</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Container</th>
|
||||||
|
<th>Seal</th>
|
||||||
|
<th>Cargo</th>
|
||||||
|
<th>Weight</th>
|
||||||
|
<th>Qty</th>
|
||||||
|
<th>Wagon</th>
|
||||||
|
<th>Condition</th>
|
||||||
|
<th>Damage / Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rows || '<tr><td colspan="11">No items</td></tr>'}</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="signatures">
|
||||||
|
<div class="signature">EDR Representative: ${escapeHtml(document.generatedBy)}</div>
|
||||||
|
<div class="signature">Djibouti Port Operator: ${escapeHtml(document.acknowledgedBy)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">Generated from EDR Freight Management System. Printed on ${escapeHtml(new Date().toLocaleString())}.</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
};
|
||||||
|
|
||||||
function DetailField({ label, value }: { label: string; value: ReactNode }) {
|
function DetailField({ label, value }: { label: string; value: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Stack gap={2}>
|
<Stack gap={2}>
|
||||||
@@ -157,6 +268,37 @@ export default function InterchangeDocumentsPage() {
|
|||||||
const dispute = useDisputeInterchangeDocument();
|
const dispute = useDisputeInterchangeDocument();
|
||||||
const cancel = useCancelInterchangeDocument();
|
const cancel = useCancelInterchangeDocument();
|
||||||
|
|
||||||
|
const getPrintableDocument = async (interchangeDocument: InterchangeDocument) => {
|
||||||
|
if (interchangeDocument.items?.length) return interchangeDocument;
|
||||||
|
return interchangeDocumentsService.getById(interchangeDocument.id).then((response) => response.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const printDocument = async (interchangeDocument: InterchangeDocument) => {
|
||||||
|
const fullDocument = await getPrintableDocument(interchangeDocument);
|
||||||
|
const win = window.open('', '_blank');
|
||||||
|
if (!win) {
|
||||||
|
toast({ variant: 'destructive', title: 'Pop-up blocked', description: 'Allow pop-ups to print the document.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
win.document.write(buildPrintableInterchangeHtml(fullDocument));
|
||||||
|
win.document.close();
|
||||||
|
win.focus();
|
||||||
|
setTimeout(() => win.print(), 250);
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadDocument = async (interchangeDocument: InterchangeDocument) => {
|
||||||
|
const fullDocument = await getPrintableDocument(interchangeDocument);
|
||||||
|
const blob = new Blob([buildPrintableInterchangeHtml(fullDocument)], { type: 'text/html;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = filenameFor(fullDocument);
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
const run = async (fn: () => Promise<unknown>, title: string) => {
|
const run = async (fn: () => Promise<unknown>, title: string) => {
|
||||||
try {
|
try {
|
||||||
await fn();
|
await fn();
|
||||||
@@ -169,10 +311,10 @@ export default function InterchangeDocumentsPage() {
|
|||||||
const acknowledgeDocument = (document: InterchangeDocument) => {
|
const acknowledgeDocument = (document: InterchangeDocument) => {
|
||||||
const acknowledgedBy = window.prompt('Acknowledged by');
|
const acknowledgedBy = window.prompt('Acknowledged by');
|
||||||
if (!acknowledgedBy) return;
|
if (!acknowledgedBy) return;
|
||||||
run(
|
run(async () => {
|
||||||
() => acknowledge.mutateAsync({ id: document.id, acknowledgedBy }),
|
const response = await acknowledge.mutateAsync({ id: document.id, acknowledgedBy });
|
||||||
'Interchange document acknowledged',
|
await printDocument(response.data);
|
||||||
);
|
}, 'Interchange document acknowledged');
|
||||||
};
|
};
|
||||||
|
|
||||||
const disputeDocument = (document: InterchangeDocument) => {
|
const disputeDocument = (document: InterchangeDocument) => {
|
||||||
@@ -284,6 +426,28 @@ export default function InterchangeDocumentsPage() {
|
|||||||
Acknowledge
|
Acknowledge
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{document.status === 'ACKNOWLEDGED' ? (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="compact-xs"
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<Printer size={14} />}
|
||||||
|
onClick={() => run(() => printDocument(document), 'Print view opened')}
|
||||||
|
>
|
||||||
|
Print
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="compact-xs"
|
||||||
|
color="blue"
|
||||||
|
variant="light"
|
||||||
|
leftSection={<Download size={14} />}
|
||||||
|
onClick={() => run(() => downloadDocument(document), 'Document downloaded')}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
{document.status !== 'CANCELLED' ? (
|
{document.status !== 'CANCELLED' ? (
|
||||||
<Button
|
<Button
|
||||||
size="compact-xs"
|
size="compact-xs"
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export const API_BASE_URL = 'https://edrfreightapi.triaplc.com';
|
export const API_BASE_URL = 'https://edrfreightapi.triaplc.com';
|
||||||
// export const API_BASE_URL = 'http://localhost:3001';
|
//export const API_BASE_URL = 'http://localhost:3001';
|
||||||
|
|||||||
Reference in New Issue
Block a user