|
|
|
|
@@ -243,11 +243,6 @@ export interface BulkReceiveResult {
|
|
|
|
|
results: { bookingId: string; status: string; inventoryId?: string; grnNumber?: string; reason?: string }[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface LoadPassedExportResult {
|
|
|
|
|
loadedCount: number;
|
|
|
|
|
skippedCount: number;
|
|
|
|
|
results: { inventoryId: string; status: string; reason?: string }[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface BulkInspectResult {
|
|
|
|
|
inspectedCount: number;
|
|
|
|
|
@@ -1098,46 +1093,6 @@ export class WarehouseInventoryService {
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Bulk-load all EXPORT inventory that passed inspection and is READY_FOR_LOADING. */
|
|
|
|
|
async loadPassedExport(performedBy?: string): Promise<LoadPassedExportResult> {
|
|
|
|
|
const ready = await this.inventoryRepository.findAll({ where: { status: 'READY_FOR_LOADING' } });
|
|
|
|
|
const result: LoadPassedExportResult = { loadedCount: 0, skippedCount: 0, results: [] };
|
|
|
|
|
|
|
|
|
|
for (const item of ready) {
|
|
|
|
|
const skip = (reason: string) => {
|
|
|
|
|
result.skippedCount += 1;
|
|
|
|
|
result.results.push({ inventoryId: item.id, status: 'SKIPPED', reason });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (item.inspectionStatus !== 'PASSED') { skip('Inspection not PASSED'); continue; }
|
|
|
|
|
const direction = item.bookingId ? await this.getBookingDirection(item.bookingId) : null;
|
|
|
|
|
if (direction !== 'EXPORT') { skip('Not an EXPORT item'); continue; }
|
|
|
|
|
const bookingStatus = item.bookingId ? await this.getBookingStatus(item.bookingId) : null;
|
|
|
|
|
if (bookingStatus !== 'PAID') { skip('Booking not PAID'); continue; }
|
|
|
|
|
|
|
|
|
|
await this.dataSource.transaction(async (manager) => {
|
|
|
|
|
await manager.getRepository(WarehouseInventory).update(item.id, {
|
|
|
|
|
status: 'LOADED',
|
|
|
|
|
loadedAt: new Date(),
|
|
|
|
|
});
|
|
|
|
|
await this.activityLog.record(
|
|
|
|
|
{
|
|
|
|
|
activityType: 'INVENTORY_LOADED',
|
|
|
|
|
inventoryId: item.id,
|
|
|
|
|
warehouseId: item.warehouseId,
|
|
|
|
|
description: 'Bulk loaded (passed export)',
|
|
|
|
|
performedBy,
|
|
|
|
|
},
|
|
|
|
|
manager,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
result.loadedCount += 1;
|
|
|
|
|
result.results.push({ inventoryId: item.id, status: 'LOADED' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** EXPORT inventory rows at a given status (route-derived direction), with booking detail. */
|
|
|
|
|
private async exportInventoryByStatus(
|
|
|
|
|
@@ -1319,6 +1274,29 @@ export class WarehouseInventoryService {
|
|
|
|
|
performedBy?: string,
|
|
|
|
|
): Promise<TrainLoadResult> {
|
|
|
|
|
const result: TrainLoadResult = { loadedCount: 0, skippedCount: 0, results: [] };
|
|
|
|
|
const [schedule]: Array<{
|
|
|
|
|
trainNumber: string | null;
|
|
|
|
|
origin: string | null;
|
|
|
|
|
destination: string | null;
|
|
|
|
|
departure: string | null;
|
|
|
|
|
}> = await this.dataSource.query(
|
|
|
|
|
`SELECT ts.train_number AS "trainNumber",
|
|
|
|
|
COALESCE(oy.label, oy.code) AS "origin",
|
|
|
|
|
COALESCE(dy.label, dy.code) AS "destination",
|
|
|
|
|
ts.scheduled_departure_date AS "departure"
|
|
|
|
|
FROM freight.train_schedules ts
|
|
|
|
|
LEFT JOIN freight.yards oy ON oy.id = ts.origin_station_id
|
|
|
|
|
LEFT JOIN freight.yards dy ON dy.id = ts.destination_station_id
|
|
|
|
|
WHERE ts.id = $1 AND ts.deleted_at IS NULL`,
|
|
|
|
|
[scheduleId],
|
|
|
|
|
);
|
|
|
|
|
const trainNote = schedule
|
|
|
|
|
? `Loaded onto train ${schedule.trainNumber ?? scheduleId.slice(0, 8)}` +
|
|
|
|
|
(schedule.origin || schedule.destination
|
|
|
|
|
? ` (${schedule.origin ?? '?'} -> ${schedule.destination ?? '?'})`
|
|
|
|
|
: '') +
|
|
|
|
|
(schedule.departure ? `, departure ${new Date(schedule.departure).toISOString()}` : '')
|
|
|
|
|
: undefined;
|
|
|
|
|
const items = await this.trainLoadableItems(scheduleId);
|
|
|
|
|
const byId = new Map(items.map((i) => [i.id, i]));
|
|
|
|
|
const affectedBookingIds = new Set<string>();
|
|
|
|
|
@@ -1335,7 +1313,12 @@ export class WarehouseInventoryService {
|
|
|
|
|
if (!item.wagonId) { skip('No wagon allocated — allocate a wagon first'); continue; }
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await this.load(inventoryId, { wagonId: item.wagonId, loadedBy: performedBy });
|
|
|
|
|
await this.load(inventoryId, {
|
|
|
|
|
wagonId: item.wagonId,
|
|
|
|
|
loadedBy: performedBy,
|
|
|
|
|
trainScheduleId: scheduleId,
|
|
|
|
|
notes: trainNote,
|
|
|
|
|
});
|
|
|
|
|
result.loadedCount += 1;
|
|
|
|
|
result.results.push({ inventoryId, status: 'LOADED' });
|
|
|
|
|
if (item.bookingId) affectedBookingIds.add(item.bookingId);
|
|
|
|
|
@@ -3403,6 +3386,8 @@ export class WarehouseInventoryService {
|
|
|
|
|
warehouseInventoryId: id,
|
|
|
|
|
bookingId: item.bookingId ?? null,
|
|
|
|
|
wagonId: dto.wagonId,
|
|
|
|
|
// Which train this load belongs to — durable even if wagons reshuffle.
|
|
|
|
|
trainScheduleId: dto.trainScheduleId ?? null,
|
|
|
|
|
loadedAt: now,
|
|
|
|
|
loadedBy: dto.loadedBy ?? null,
|
|
|
|
|
loadedWeight,
|
|
|
|
|
@@ -3441,7 +3426,7 @@ export class WarehouseInventoryService {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Enrich with wagon numbers (read-only lookup into the scheduling domain).
|
|
|
|
|
const wagonIds = [...new Set(loadings.map((l) => l.wagonId))];
|
|
|
|
|
const wagonIds = [...new Set(loadings.map((l) => l.wagonId).filter((id): id is string => Boolean(id)))];
|
|
|
|
|
const wagonNumbers = new Map<string, string>();
|
|
|
|
|
if (wagonIds.length > 0) {
|
|
|
|
|
const rows: Array<{ id: string; wagon_number: string }> = await this.dataSource.query(
|
|
|
|
|
@@ -3452,7 +3437,7 @@ export class WarehouseInventoryService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return loadings.map((loading) =>
|
|
|
|
|
Object.assign(loading, { wagonNumber: wagonNumbers.get(loading.wagonId) ?? null }),
|
|
|
|
|
Object.assign(loading, { wagonNumber: (loading.wagonId && wagonNumbers.get(loading.wagonId)) ?? null }),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|