mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 03:40:56 +00:00
enhance shipment requests page with filtering and sorting options
- Added status and cargo filters to the ShipmentRequestsPage. - Implemented date range filtering for preferred dates. - Introduced sorting options for shipment requests based on submission date and reference. - Enhanced the display of shipment request details, including status badges and customer information. - Updated the UI to include a search input with clear functionality and improved layout for filters. feat: add equipment return option in new shipment form - Introduced a toggle for equipment return in the NewShipmentPage. - Updated form schema to include field for container contracts. - Enhanced user experience with visual feedback on the equipment return selection. fix: update booking DTO to include equipment return option - Added field to CreateBookingUnderContractDto for per-shipment override. - Updated related types and schemas to accommodate the new field for better contract handling.
This commit is contained in:
@@ -31,7 +31,6 @@ import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity
|
||||
import { TrainScheduleBooking } from '../train-schedules/entities/train-schedule-booking.entity';
|
||||
import { TrainSchedulesRepository } from '../train-schedules/train-schedules.repository';
|
||||
import { TrainScheduleBookingsRepository } from '../train-schedules/train-schedule-bookings.repository';
|
||||
import { TrainSchedulingGlobalRules } from './entities/train-scheduling-global-rules.entity';
|
||||
import { BookingNotifierService } from './booking-notifier.service';
|
||||
import { TrainSchedulingService } from './train-scheduling.service';
|
||||
import { eatDay, groupBookingsIntoBoardWindows } from './batch-window.util';
|
||||
@@ -568,7 +567,6 @@ export class BookingBatchService implements OnModuleInit {
|
||||
);
|
||||
}
|
||||
|
||||
const rules = await this.loadGlobalRules();
|
||||
const wagonDims = await this.loadWagonDims();
|
||||
const required = need ?? this.needFor(booking, wagonDims);
|
||||
let corridorMatched = false;
|
||||
@@ -578,7 +576,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
);
|
||||
const locomotive = schedule?.trainSet?.locomotive;
|
||||
if (!schedule || !locomotive) continue;
|
||||
const limits = await this.capacityLimits(locomotive, rules);
|
||||
const limits = await this.capacityLimits(locomotive);
|
||||
const budget = await this.remainingBudget(schedule, limits, wagonDims);
|
||||
const leg = budget.legOf(booking.originYardId, booking.destinationYardId);
|
||||
if (!leg) continue; // this train's route doesn't carry the booking's leg
|
||||
@@ -757,7 +755,6 @@ export class BookingBatchService implements OnModuleInit {
|
||||
});
|
||||
|
||||
const wagonDims = await this.loadWagonDims();
|
||||
const rules = await this.loadGlobalRules();
|
||||
const linkRepo = this.dataSource.getRepository(TrainScheduleBooking);
|
||||
|
||||
const board: BatchBoardSchedule[] = [];
|
||||
@@ -787,7 +784,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
};
|
||||
});
|
||||
|
||||
board.push(this.buildScheduleSummary(s, items, rules));
|
||||
board.push(this.buildScheduleSummary(s, items));
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -817,7 +814,6 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}
|
||||
|
||||
const wagonDims = await this.loadWagonDims();
|
||||
const rules = await this.loadGlobalRules();
|
||||
const linkRepo = this.dataSource.getRepository(TrainScheduleBooking);
|
||||
const links = await linkRepo.find({ where: { trainScheduleId: s.id } });
|
||||
const linkedIds = new Set(links.map((l) => l.bookingId));
|
||||
@@ -1011,7 +1007,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
|
||||
}
|
||||
: null,
|
||||
capacity: this.computeBoardCapacity(items, loco, s.maxWagons ?? null, rules),
|
||||
capacity: this.computeBoardCapacity(items, loco, s.maxWagons ?? null),
|
||||
counts: {
|
||||
allocated: items.filter((i) => i.state === "ALLOCATED").length,
|
||||
selectedForBatch: items.filter((i) => i.state === "SELECTED_FOR_BATCH")
|
||||
@@ -1045,9 +1041,10 @@ export class BookingBatchService implements OnModuleInit {
|
||||
/**
|
||||
* Board capacity figures. `usedWeightTons` is GROSS (each item's weight already
|
||||
* includes the tare of the wagons it occupies), so the ceiling it is measured
|
||||
* against must be the same one the fill loop spends from: the locomotive floored
|
||||
* by the global rule caps and widened by its overage tolerance. Reading the raw
|
||||
* `loco.maxPullWeightTons` here showed staff a ceiling the batch engine did not use.
|
||||
* against must be the same one the fill loop spends from: the locomotive's own
|
||||
* limits widened by its overage tolerance (global rule caps do not apply, same
|
||||
* as {@link capacityLimits}). Reading the raw `loco.maxPullWeightTons` here
|
||||
* showed staff a ceiling the batch engine did not use.
|
||||
*/
|
||||
private computeBoardCapacity(
|
||||
items: Array<{
|
||||
@@ -1058,29 +1055,18 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}>,
|
||||
loco: Locomotive | null,
|
||||
maxWagons: number | null,
|
||||
rules: TrainSchedulingGlobalRules | null,
|
||||
): BatchBoardSchedule["capacity"] {
|
||||
const allocated = items.filter((i) => i.state === "ALLOCATED");
|
||||
const committed = items.filter(
|
||||
(i) => i.state === "ALLOCATED" || i.state === "SELECTED_FOR_BATCH",
|
||||
);
|
||||
const caps = loco
|
||||
? trainHardCaps(
|
||||
{
|
||||
maxPullWeightTons: Number(loco.maxPullWeightTons),
|
||||
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
|
||||
overageToleranceTons: Number(loco.overageToleranceTons) || 0,
|
||||
overageToleranceMeters: Number(loco.overageToleranceMeters) || 0,
|
||||
},
|
||||
{
|
||||
maxTrainWeightTons: rules?.maxTrainWeightTons
|
||||
? Number(rules.maxTrainWeightTons)
|
||||
: undefined,
|
||||
maxTrainLengthMeters: rules?.maxTrainLengthMeters
|
||||
? Number(rules.maxTrainLengthMeters)
|
||||
: undefined,
|
||||
},
|
||||
)
|
||||
? trainHardCaps({
|
||||
maxPullWeightTons: Number(loco.maxPullWeightTons),
|
||||
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
|
||||
overageToleranceTons: Number(loco.overageToleranceTons) || 0,
|
||||
overageToleranceMeters: Number(loco.overageToleranceMeters) || 0,
|
||||
})
|
||||
: null;
|
||||
const round2 = (value: number) => Math.round(value * 100) / 100;
|
||||
|
||||
@@ -1099,7 +1085,6 @@ export class BookingBatchService implements OnModuleInit {
|
||||
private buildScheduleSummary(
|
||||
s: TrainSchedule,
|
||||
items: BatchBoardBooking[],
|
||||
rules: TrainSchedulingGlobalRules | null,
|
||||
): BatchBoardSchedule {
|
||||
const loco = s.trainSet?.locomotive ?? null;
|
||||
|
||||
@@ -1134,7 +1119,7 @@ export class BookingBatchService implements OnModuleInit {
|
||||
maxTrainLengthMeters: Number(loco.maxTrainLengthMeters),
|
||||
}
|
||||
: null,
|
||||
capacity: this.computeBoardCapacity(items, loco, s.maxWagons ?? null, rules),
|
||||
capacity: this.computeBoardCapacity(items, loco, s.maxWagons ?? null),
|
||||
counts: {
|
||||
allocated: items.filter((i) => i.state === "ALLOCATED").length,
|
||||
selectedForBatch: items.filter((i) => i.state === "SELECTED_FOR_BATCH")
|
||||
@@ -1203,10 +1188,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const rules = await this.loadGlobalRules();
|
||||
const wagonDims = await this.loadWagonDims();
|
||||
const limits = await this.capacityLimits(locomotive, rules);
|
||||
await this.syncScheduleMaxWagons(schedule, locomotive, rules);
|
||||
const limits = await this.capacityLimits(locomotive);
|
||||
await this.syncScheduleMaxWagons(schedule, locomotive);
|
||||
const budget = await this.remainingBudget(schedule, limits, wagonDims);
|
||||
const minPerWagon = this.minPerWagonNeed(wagonDims);
|
||||
if (budget.isExhausted(minPerWagon)) {
|
||||
@@ -1405,7 +1389,6 @@ export class BookingBatchService implements OnModuleInit {
|
||||
return { scheduleIds: [], commercialReserved: 0 };
|
||||
}
|
||||
|
||||
const rules = await this.loadGlobalRules();
|
||||
const wagonDims = await this.loadWagonDims();
|
||||
|
||||
// Live per-schedule corridor budget + arm flag, in departure order.
|
||||
@@ -1420,8 +1403,8 @@ export class BookingBatchService implements OnModuleInit {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const limits = await this.capacityLimits(locomotive, rules);
|
||||
await this.syncScheduleMaxWagons(schedule, locomotive, rules);
|
||||
const limits = await this.capacityLimits(locomotive);
|
||||
await this.syncScheduleMaxWagons(schedule, locomotive);
|
||||
const budget = await this.remainingBudget(schedule, limits, wagonDims);
|
||||
trains.push({ id, budget, armed: false });
|
||||
}
|
||||
@@ -1973,9 +1956,8 @@ export class BookingBatchService implements OnModuleInit {
|
||||
await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
const locomotive = schedule?.trainSet?.locomotive;
|
||||
if (!schedule || !locomotive) return null;
|
||||
const rules = await this.loadGlobalRules();
|
||||
const wagonDims = await this.loadWagonDims();
|
||||
const limits = await this.capacityLimits(locomotive, rules);
|
||||
const limits = await this.capacityLimits(locomotive);
|
||||
const budget = await this.remainingBudget(schedule, limits, wagonDims);
|
||||
return { budget, needFor: (booking) => this.needFor(booking, wagonDims) };
|
||||
}
|
||||
@@ -2520,11 +2502,12 @@ export class BookingBatchService implements OnModuleInit {
|
||||
* `base` via {@link needFor}, whose weight axis is gross. The locomotive's
|
||||
* overage tolerance is returned separately — the corridor budget spends it
|
||||
* only to admit a booking whole, never to size a split.
|
||||
*
|
||||
* Limits come from the LOCOMOTIVE ALONE — the global-rules weight/length
|
||||
* caps deliberately do not apply here (a mis-set global row once capped
|
||||
* every train at 14m and no export booking could board).
|
||||
*/
|
||||
private async capacityLimits(
|
||||
locomotive: Locomotive,
|
||||
rules: TrainSchedulingGlobalRules | null,
|
||||
): Promise<TrainLimits> {
|
||||
private async capacityLimits(locomotive: Locomotive): Promise<TrainLimits> {
|
||||
const wagonTypes = await this.loadWagonTypeDimensions();
|
||||
const derived = deriveTrainCapacityFromLocomotive(
|
||||
{
|
||||
@@ -2534,14 +2517,6 @@ export class BookingBatchService implements OnModuleInit {
|
||||
overageToleranceMeters: Number(locomotive.overageToleranceMeters) || 0,
|
||||
},
|
||||
wagonTypes,
|
||||
{
|
||||
maxTrainWeightTons: rules?.maxTrainWeightTons
|
||||
? Number(rules.maxTrainWeightTons)
|
||||
: undefined,
|
||||
maxTrainLengthMeters: rules?.maxTrainLengthMeters
|
||||
? Number(rules.maxTrainLengthMeters)
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
return {
|
||||
base: {
|
||||
@@ -2556,18 +2531,27 @@ export class BookingBatchService implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
/** Keep schedule.max_wagons aligned with locomotive physical limits. */
|
||||
/**
|
||||
* Keep schedule.max_wagons aligned with the train's real boarding limit: the
|
||||
* locomotive's length-derived slot count, floored by the physical wagons in
|
||||
* the train set (slots that exist on paper but not in the yard must not be
|
||||
* sold — see {@link remainingBudget}).
|
||||
*/
|
||||
private async syncScheduleMaxWagons(
|
||||
schedule: TrainSchedule,
|
||||
locomotive: Locomotive,
|
||||
rules: TrainSchedulingGlobalRules | null,
|
||||
): Promise<void> {
|
||||
const limits = await this.capacityLimits(locomotive, rules);
|
||||
if ((schedule.maxWagons ?? 0) !== limits.base.wagons) {
|
||||
const limits = await this.capacityLimits(locomotive);
|
||||
const physicalWagons = schedule.trainSet?.wagons?.length ?? 0;
|
||||
const maxWagons =
|
||||
physicalWagons > 0
|
||||
? Math.min(limits.base.wagons, physicalWagons)
|
||||
: limits.base.wagons;
|
||||
if ((schedule.maxWagons ?? 0) !== maxWagons) {
|
||||
await this.dataSource
|
||||
.getRepository(TrainSchedule)
|
||||
.update(schedule.id, { maxWagons: limits.base.wagons });
|
||||
schedule.maxWagons = limits.base.wagons;
|
||||
.update(schedule.id, { maxWagons });
|
||||
schedule.maxWagons = maxWagons;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2655,12 +2639,6 @@ export class BookingBatchService implements OnModuleInit {
|
||||
};
|
||||
}
|
||||
|
||||
private async loadGlobalRules(): Promise<TrainSchedulingGlobalRules | null> {
|
||||
return this.dataSource
|
||||
.getRepository(TrainSchedulingGlobalRules)
|
||||
.findOne({ where: {} });
|
||||
}
|
||||
|
||||
/**
|
||||
* Ordered stop yards of the schedule's route (origin → milestones →
|
||||
* destination); the legacy two-stop pseudo-route when milestones are absent.
|
||||
@@ -2684,6 +2662,12 @@ export class BookingBatchService implements OnModuleInit {
|
||||
* Remaining capacity per corridor edge = hard caps minus what allocated +
|
||||
* reserved bookings already use ON THEIR OWN LEGS. A booking riding only
|
||||
* Dire→Djibouti leaves the Addis→Dire edges untouched.
|
||||
*
|
||||
* The wagon axis is additionally capped by the PHYSICAL wagons marshalled in
|
||||
* the schedule's train set. The length-derived slot count says how many wagons
|
||||
* the locomotive could pull, not how many exist: a 760m/54-slot train with a
|
||||
* 50-wagon set once split-offered 4 wagons that were never buildable — the
|
||||
* customer paid and the wagon planner had nothing to assign.
|
||||
*/
|
||||
private async remainingBudget(
|
||||
schedule: TrainSchedule,
|
||||
@@ -2691,7 +2675,12 @@ export class BookingBatchService implements OnModuleInit {
|
||||
wagonDims: WagonDims,
|
||||
): Promise<CorridorBudget> {
|
||||
const stops = await this.stopsForSchedule(schedule);
|
||||
const budget = new CorridorBudget(stops, limits.base, limits.tolerance);
|
||||
const physicalWagons = schedule.trainSet?.wagons?.length ?? 0;
|
||||
const base =
|
||||
physicalWagons > 0
|
||||
? { ...limits.base, wagons: Math.min(limits.base.wagons, physicalWagons) }
|
||||
: limits.base;
|
||||
const budget = new CorridorBudget(stops, base, limits.tolerance);
|
||||
const allocated = (schedule.scheduleBookings ?? [])
|
||||
.map((sb) => sb.booking)
|
||||
.filter((b): b is Booking => Boolean(b));
|
||||
@@ -2795,9 +2784,8 @@ export class BookingBatchService implements OnModuleInit {
|
||||
if ((await this.remainingWagons(schedule)) <= 0) return true;
|
||||
const locomotive = schedule.trainSet?.locomotive;
|
||||
if (!locomotive) return false; // no weight/length limits to bind against
|
||||
const rules = await this.loadGlobalRules();
|
||||
const wagonDims = await this.loadWagonDims();
|
||||
const limits = await this.capacityLimits(locomotive, rules);
|
||||
const limits = await this.capacityLimits(locomotive);
|
||||
const budget = await this.remainingBudget(schedule, limits, wagonDims);
|
||||
return budget.isExhausted(this.minPerWagonNeed(wagonDims));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
NotificationAudience,
|
||||
NotificationPriority,
|
||||
NotificationType,
|
||||
NotifyInput,
|
||||
} from '@edr/types';
|
||||
@@ -114,11 +115,15 @@ export class BookingNotifierService {
|
||||
const eat = deadline.toLocaleString('en-GB', { timeZone: 'Africa/Addis_Ababa' });
|
||||
const msg =
|
||||
`Only ${offeredWagons} of ${totalWagons} wagons fit the train for booking ${b.reference ?? b.id}. ` +
|
||||
`Pay within ${payMinutes} minute${payMinutes === 1 ? '' : 's'} to accept and ship ${offeredWagons} wagon${offeredWagons === 1 ? '' : 's'} now ` +
|
||||
`(the rest returns to your contract to book later). If you do not pay, the booking stays whole and you can rebook in the next window. Deadline: ${eat} EAT.`;
|
||||
`Pay within ${payMinutes} minute${payMinutes === 1 ? '' : 's'} to accept and ship ${offeredWagons} wagon${offeredWagons === 1 ? '' : 's'} now. ` +
|
||||
`The remaining ${totalWagons - offeredWagons} return${totalWagons - offeredWagons === 1 ? 's' : ''} to your contract — book them yourself in a later window. ` +
|
||||
`If you do not pay, the booking stays whole and you can rebook in the next window. Deadline: ${eat} EAT.`;
|
||||
await this.notifyContact(b, msg, 'PAY NOW (PARTIAL)');
|
||||
// HIGH: a split is a change to what the customer ordered AND a live payment
|
||||
// deadline — it must reach email/SMS, not just the portal inbox.
|
||||
this.inApp(b, 'Partial allocation offer', msg, {
|
||||
type: NotificationType.INVOICE_ISSUED,
|
||||
priority: NotificationPriority.HIGH,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3111,6 +3111,10 @@ export class TrainSchedulingService {
|
||||
const wagonTypes = await this.loadSchedulingWagonTypeDimensions();
|
||||
|
||||
if (locomotive) {
|
||||
// With a locomotive assigned its own limits are the single source of
|
||||
// truth — global-rules / env caps do not floor them (a mis-set global
|
||||
// row once capped every train at 14m). Only an explicit per-request dto
|
||||
// override still applies.
|
||||
const derived = deriveTrainCapacityFromLocomotive(
|
||||
{
|
||||
maxPullWeightTons: Number(locomotive.maxPullWeightTons),
|
||||
@@ -3120,8 +3124,8 @@ export class TrainSchedulingService {
|
||||
},
|
||||
wagonTypes,
|
||||
{
|
||||
maxTrainWeightTons: ruleWeightCap,
|
||||
maxTrainLengthMeters: ruleLengthCap,
|
||||
maxTrainWeightTons: dto?.maxTrainWeightTons,
|
||||
maxTrainLengthMeters: dto?.maxTrainLengthMeters,
|
||||
},
|
||||
);
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user