mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge branch 'dev'
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
FindOptionsWhere,
|
||||
ILike,
|
||||
In,
|
||||
IsNull,
|
||||
LessThanOrEqual,
|
||||
MoreThanOrEqual,
|
||||
} from 'typeorm';
|
||||
@@ -820,8 +821,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
// stop order, so we fetch the day's open trains without endpoint filters.
|
||||
const corridor = await this.trainSchedulesRepository.findAll({
|
||||
where: [
|
||||
{ status: TrainScheduleStatusEnum.Draft },
|
||||
{ status: TrainScheduleStatusEnum.Scheduled },
|
||||
// Dedicated shipping-line trains are never customer-booking targets.
|
||||
{ status: TrainScheduleStatusEnum.Draft, shippingLineCompanyId: IsNull() },
|
||||
{ status: TrainScheduleStatusEnum.Scheduled, shippingLineCompanyId: IsNull() },
|
||||
],
|
||||
});
|
||||
// A customer-picked train narrows the scan to that ONE schedule: export
|
||||
@@ -983,8 +985,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
): Promise<Array<{ scheduleId: string; departure: Date; freeWagons: number }>> {
|
||||
const corridor = await this.trainSchedulesRepository.findAll({
|
||||
where: [
|
||||
{ status: TrainScheduleStatusEnum.Draft },
|
||||
{ status: TrainScheduleStatusEnum.Scheduled },
|
||||
// Dedicated shipping-line trains are never customer-booking targets.
|
||||
{ status: TrainScheduleStatusEnum.Draft, shippingLineCompanyId: IsNull() },
|
||||
{ status: TrainScheduleStatusEnum.Scheduled, shippingLineCompanyId: IsNull() },
|
||||
],
|
||||
});
|
||||
const candidates = corridor
|
||||
@@ -1094,8 +1097,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
}
|
||||
const corridor = await this.trainSchedulesRepository.findAll({
|
||||
where: [
|
||||
{ status: TrainScheduleStatusEnum.Draft },
|
||||
{ status: TrainScheduleStatusEnum.Scheduled },
|
||||
// Dedicated shipping-line trains are never customer-booking targets.
|
||||
{ status: TrainScheduleStatusEnum.Draft, shippingLineCompanyId: IsNull() },
|
||||
{ status: TrainScheduleStatusEnum.Scheduled, shippingLineCompanyId: IsNull() },
|
||||
],
|
||||
});
|
||||
const candidates = corridor
|
||||
@@ -1219,8 +1223,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
): Promise<{ freeWagons: number; need: number; trainsForDay: boolean }> {
|
||||
const corridor = await this.trainSchedulesRepository.findAll({
|
||||
where: [
|
||||
{ status: TrainScheduleStatusEnum.Draft },
|
||||
{ status: TrainScheduleStatusEnum.Scheduled },
|
||||
// Dedicated shipping-line trains are never customer-booking targets.
|
||||
{ status: TrainScheduleStatusEnum.Draft, shippingLineCompanyId: IsNull() },
|
||||
{ status: TrainScheduleStatusEnum.Scheduled, shippingLineCompanyId: IsNull() },
|
||||
],
|
||||
});
|
||||
const candidates = corridor.filter(
|
||||
@@ -2365,11 +2370,14 @@ export class BookingBatchService implements OnModuleInit {
|
||||
originStationId: originYardId,
|
||||
destinationStationId: destinationYardId,
|
||||
status: TrainScheduleStatusEnum.Draft,
|
||||
// Dedicated shipping-line trains never join the customer day pool.
|
||||
shippingLineCompanyId: IsNull(),
|
||||
},
|
||||
{
|
||||
originStationId: originYardId,
|
||||
destinationStationId: destinationYardId,
|
||||
status: TrainScheduleStatusEnum.Scheduled,
|
||||
shippingLineCompanyId: IsNull(),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -3100,8 +3108,9 @@ export class BookingBatchService implements OnModuleInit {
|
||||
if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`);
|
||||
const schedules = await this.trainSchedulesRepository.findAll({
|
||||
where: [
|
||||
{ status: TrainScheduleStatusEnum.Draft },
|
||||
{ status: TrainScheduleStatusEnum.Scheduled },
|
||||
// Dedicated shipping-line trains are never customer-booking targets.
|
||||
{ status: TrainScheduleStatusEnum.Draft, shippingLineCompanyId: IsNull() },
|
||||
{ status: TrainScheduleStatusEnum.Scheduled, shippingLineCompanyId: IsNull() },
|
||||
],
|
||||
});
|
||||
const today = eatDay(new Date());
|
||||
|
||||
@@ -11,6 +11,8 @@ describe('BookingJourneyService.autoPlaceOnFreedWagons', () => {
|
||||
{} as never, // yardFacilities
|
||||
{} as never, // facilityHandling
|
||||
{ emit: jest.fn() } as never, // events
|
||||
{} as never, // notifications
|
||||
{} as never, // inbox
|
||||
);
|
||||
|
||||
const schedule = { id: 'sched-1', trainSetId: 'ts-1' };
|
||||
|
||||
@@ -24,7 +24,10 @@ import { WagonBookingAllocation } from '../train-schedules/entities/wagon-bookin
|
||||
import { Wagon } from '../wagons/entities/wagon.entity';
|
||||
import { WagonMovement } from '../wagons/entities/wagon-movement.entity';
|
||||
import { TrainCheckpointEvent } from './entities/train-checkpoint-event.entity';
|
||||
import { assertExportReceivedWithGrn } from '../../common/export-received-gate';
|
||||
import { assertExportReceivedWithGrn, DIRECT_TO_TRAIN } from '../../common/export-received-gate';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { NotificationInboxService } from '../notification-inbox/notification-inbox.service';
|
||||
import { notifyCarriageAcceptanceReady } from '../notifications/notify-company.util';
|
||||
|
||||
/**
|
||||
* Per-booking journey along a train's corridor — for EVERY trade direction.
|
||||
@@ -52,6 +55,8 @@ export class BookingJourneyService {
|
||||
private readonly yardFacilities: YardFacilitiesService,
|
||||
private readonly facilityHandling: FacilityHandlingService,
|
||||
private readonly events: EventEmitter2,
|
||||
private readonly notifications: NotificationsService,
|
||||
private readonly inbox: NotificationInboxService,
|
||||
@Optional() private readonly milestoneService?: ClearanceMilestoneService,
|
||||
) {}
|
||||
|
||||
@@ -76,6 +81,21 @@ export class BookingJourneyService {
|
||||
// Export cargo must be in the warehouse with a GRN before it can be loaded,
|
||||
// however it arrived and whatever it is allocated to.
|
||||
await assertExportReceivedWithGrn(this.dataSource, booking);
|
||||
// Direct truck-to-train cargo never sees the warehouse, so loading IS its
|
||||
// handover moment — the carriage acceptance sheet must go out to the
|
||||
// customer right here, not on a receive event that will never fire.
|
||||
if (
|
||||
booking.tradeDirection === 'EXPORT' &&
|
||||
booking.exportHandoverMode === DIRECT_TO_TRAIN
|
||||
) {
|
||||
await notifyCarriageAcceptanceReady(
|
||||
this.dataSource,
|
||||
this.notifications,
|
||||
this.inbox,
|
||||
booking.id,
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
|
||||
@@ -172,6 +172,17 @@ export class CreateContainerTrainScheduleDto {
|
||||
@IsBoolean()
|
||||
reverseWagonOrder?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
format: 'uuid',
|
||||
description:
|
||||
'Dedicate this departure to one shipping line. The schedule is then hidden ' +
|
||||
'from every customer-facing read (windows, day pools, home cards) and shown ' +
|
||||
'only to that shipping line in its portal. Omit for a normal customer train.',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsUUID()
|
||||
shippingLineCompanyId?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: CreateScheduleWindowRuleDto,
|
||||
description:
|
||||
|
||||
@@ -181,6 +181,7 @@ describe('TrainSchedulingService', () => {
|
||||
autoArriveAtFinalYard: jest.fn().mockResolvedValue([]),
|
||||
} as never, // bookingJourneyService
|
||||
{ dispatched: jest.fn(), arrived: jest.fn() } as never, // bookingNotifier
|
||||
{ getLogoImageUrl: jest.fn().mockResolvedValue(null) } as never, // logoSettings
|
||||
);
|
||||
|
||||
const defaultFleetWagons = [
|
||||
|
||||
@@ -47,6 +47,7 @@ import { Container } from '../../container-management/entities/container.entity'
|
||||
import { Locomotive } from '../../locomotives/entities/locomotive.entity';
|
||||
import { LocomotivesRepository } from '../../locomotives/locomotives.repository';
|
||||
import { formatRouteLabel, Route } from '../../routes/entities/route.entity';
|
||||
import { ShippingLineCompany } from '../../shipping-lines/entities/shipping-line-company.entity';
|
||||
import { WagonMovement } from '../../wagons/entities/wagon-movement.entity';
|
||||
import { Train } from '../../trains/entities/train.entity';
|
||||
import { TrainSetLocomotive } from '../../train-sets/entities/train-set-locomotive.entity';
|
||||
@@ -129,6 +130,7 @@ import {
|
||||
perEdgeConsistUsage,
|
||||
validateContainerPlacements,
|
||||
validateMixedTrainLimitsPerEdge,
|
||||
MAX_TEU_SLOTS_PER_WAGON,
|
||||
type ContainerPlacementInput,
|
||||
type WagonPlanSlot,
|
||||
} from '../utils/wagon-plan.util';
|
||||
@@ -176,6 +178,8 @@ import { RouteMilestone } from '../../routes/entities/route-milestone.entity';
|
||||
import { deriveTradeDirection } from '../../../common/derive-trade-direction.util';
|
||||
import { WarehouseInventoryService } from '../../warehouses/warehouse-inventory.service';
|
||||
import { WarehouseReleaseDocumentService } from '../../warehouses/warehouse-release-document.service';
|
||||
import { LogoSettingsService } from '../../logo-settings/logo-settings.service';
|
||||
import { logoImageCss, logoMarkup } from '../../billing/documents/logo-markup.util';
|
||||
import {
|
||||
autoFillPlacements,
|
||||
findMissingContainerNumberIssues,
|
||||
@@ -375,6 +379,7 @@ export class TrainSchedulingService {
|
||||
private readonly bookingWindowGateway: BookingWindowGateway,
|
||||
private readonly bookingJourneyService: BookingJourneyService,
|
||||
private readonly bookingNotifier: BookingNotifierService,
|
||||
private readonly logoSettings: LogoSettingsService,
|
||||
@Optional() private readonly milestoneService?: ClearanceMilestoneService,
|
||||
private readonly configService?: ConfigService,
|
||||
// forwardRef: BookingBatchService injects this service back; @Optional so
|
||||
@@ -1457,6 +1462,24 @@ export class TrainSchedulingService {
|
||||
|
||||
const scheduleWarnings: string[] = [];
|
||||
|
||||
// Dedicating the departure to a shipping line: the id comes from the
|
||||
// request, so verify it is a real, active line before stamping it.
|
||||
if (dto.shippingLineCompanyId) {
|
||||
const line = await this.dataSource
|
||||
.getRepository(ShippingLineCompany)
|
||||
.findOne({ where: { id: dto.shippingLineCompanyId } });
|
||||
if (!line) {
|
||||
throw new NotFoundException(
|
||||
`Shipping line ${dto.shippingLineCompanyId} not found`,
|
||||
);
|
||||
}
|
||||
if (line.status !== 'active') {
|
||||
throw new BadRequestException(
|
||||
`Shipping line ${line.name} is suspended — it cannot be assigned a train`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The pulling set comes either from a built train (Train Builder) or from
|
||||
// hand-picked locomotive ids (legacy path). A built train also links the
|
||||
// schedule's train set back to it (`train_sets.train_id`) so its lifecycle
|
||||
@@ -1721,6 +1744,7 @@ export class TrainSchedulingService {
|
||||
trainNumber: pairTrainNumber ?? undefined,
|
||||
maxWagons,
|
||||
reverseWagonOrder: dto.reverseWagonOrder ?? false,
|
||||
shippingLineCompanyId: dto.shippingLineCompanyId ?? null,
|
||||
...windowFields,
|
||||
}),
|
||||
);
|
||||
@@ -2930,6 +2954,10 @@ export class TrainSchedulingService {
|
||||
// dispatch pre-check keeps reporting these bookings as unloaded).
|
||||
const wagonAssignedIds = await this.getWagonAssignedBookingIds(scheduleId);
|
||||
if (wagonAssignedIds.size) {
|
||||
// Re-check the 1×40ft/2×20ft-per-wagon packing rule right before loading
|
||||
// is confirmed — allocation time already enforces it, but a wagon swap or
|
||||
// an edited allocation since then could have broken it unnoticed.
|
||||
await this.assertWagonContainerCapacity(scheduleId);
|
||||
// Export cargo must be received at the warehouse with a GRN before it can
|
||||
// be confirmed loaded — an allocation is not proof the goods are in hand.
|
||||
if (this.isExportSchedule(schedule)) {
|
||||
@@ -2997,6 +3025,9 @@ export class TrainSchedulingService {
|
||||
.map((wagon) => ({
|
||||
sequenceNo: wagon.sequenceNo,
|
||||
wagonNumber: wagon.physicalWagon?.wagonNumber ?? null,
|
||||
wagonType: wagon.wagonType?.code ?? wagon.wagonType?.name ?? null,
|
||||
tareWeightTons: wagon.wagonType?.tareWeightTons ?? null,
|
||||
equatedLengthM: wagon.wagonType?.equatedLengthM ?? null,
|
||||
allocations: (wagon.allocations ?? []).map((allocation) => ({
|
||||
bookingId: allocation.bookingId,
|
||||
bookingReference: allocation.booking?.reference ?? null,
|
||||
@@ -3017,7 +3048,7 @@ export class TrainSchedulingService {
|
||||
const loadList = await this.generateImportLoadList(scheduleId, {
|
||||
performedBy: 'DOCUMENT_GENERATION',
|
||||
});
|
||||
const html = this.buildImportLoadListHtml(loadList);
|
||||
const html = this.buildImportLoadListHtml(loadList, await this.logoSettings.getLogoImageUrl());
|
||||
// Styled table-aware fallback (marshalling grid) when Chromium is unavailable —
|
||||
// NOT the release-order fallback (would mislabel this as a gate-clearance order).
|
||||
const buffer = await this.pdfDocuments.renderTabularDocument(html, 'Import marshalling / load list');
|
||||
@@ -3037,7 +3068,9 @@ export class TrainSchedulingService {
|
||||
throw new BadRequestException('Export marshalling document applies only to EXPORT schedules');
|
||||
}
|
||||
|
||||
const html = this.buildExportLoadListHtml(schedule);
|
||||
const html = this.buildExportLoadListHtml(schedule, {
|
||||
logoImageUrl: await this.logoSettings.getLogoImageUrl(),
|
||||
});
|
||||
// Styled table-aware fallback (marshalling grid) — see importLoadListDocument.
|
||||
const buffer = await this.pdfDocuments.renderTabularDocument(html, 'Export marshalling / load list');
|
||||
const reference = schedule.trainNumber ?? schedule.id;
|
||||
@@ -3109,6 +3142,7 @@ export class TrainSchedulingService {
|
||||
positionLabel,
|
||||
wagons,
|
||||
unassignedBookings,
|
||||
logoImageUrl: await this.logoSettings.getLogoImageUrl(),
|
||||
});
|
||||
// Styled table-aware fallback (marshalling grid) — see importLoadListDocument.
|
||||
const buffer = await this.pdfDocuments.renderTabularDocument(html, 'Intercity marshalling / load list');
|
||||
@@ -3152,6 +3186,7 @@ export class TrainSchedulingService {
|
||||
positionLabel?: string;
|
||||
wagons?: TrainSetWagon[];
|
||||
unassignedBookings?: Booking[];
|
||||
logoImageUrl?: string | null;
|
||||
},
|
||||
): string {
|
||||
const esc = (value: unknown) =>
|
||||
@@ -3270,6 +3305,7 @@ export class TrainSchedulingService {
|
||||
.tile { border: 1px solid #cbd5e1; padding: 8px; min-height: 50px; }
|
||||
.tile span { display: block; color: #64748b; font-size: 9px; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 4px; }
|
||||
.tile strong { font-size: 11px; }
|
||||
${logoImageCss()}
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { background: #f8fafc; color: #475569; text-align: left; }
|
||||
th, td { border: 1px solid #cbd5e1; padding: 5px 6px; font-size: 9.5px; vertical-align: top; }
|
||||
@@ -3284,6 +3320,7 @@ export class TrainSchedulingService {
|
||||
<body>
|
||||
<div class="top">
|
||||
<div>
|
||||
${logoMarkup(opts?.logoImageUrl)}
|
||||
<div class="brand">Ethio-Djibouti Railway S.C.</div>
|
||||
<h1>${esc(opts?.title ?? 'Export Marshalling Document / Load List')}</h1>
|
||||
</div>
|
||||
@@ -3397,7 +3434,76 @@ export class TrainSchedulingService {
|
||||
}
|
||||
}
|
||||
|
||||
private buildImportLoadListHtml(loadList: Awaited<ReturnType<TrainSchedulingService['generateImportLoadList']>>): string {
|
||||
/**
|
||||
* Re-check the 1×40ft / 2×20ft-per-wagon packing rule at loading confirmation
|
||||
* time. `validateContainerPlacements` already enforces this the moment a
|
||||
* booking is allocated to a wagon, but nothing re-checks it afterwards — a
|
||||
* wagon swap, an edited allocation, or a container item added out-of-band
|
||||
* between allocation and loading could still leave a wagon over its 2-TEU
|
||||
* capacity undetected until the train is already loaded. This closes that gap
|
||||
* by summing the TEU actually persisted per wagon (40ft = 2 TEU, 20ft = 1 TEU,
|
||||
* same size resolution as the marshalling document) right before loading is
|
||||
* confirmed.
|
||||
*/
|
||||
private async assertWagonContainerCapacity(scheduleId: string): Promise<void> {
|
||||
const rows: Array<{ sequenceNo: number; wagonNumber: string | null; teuUsed: string }> =
|
||||
await this.dataSource.query(
|
||||
`SELECT tsw.sequence_no AS "sequenceNo",
|
||||
pw.wagon_number AS "wagonNumber",
|
||||
SUM(
|
||||
CASE COALESCE(ct.size_ft, bct.size_ft)
|
||||
WHEN 40 THEN 2
|
||||
WHEN 20 THEN 1
|
||||
ELSE
|
||||
CASE
|
||||
WHEN bc.container_size ILIKE '%40%' THEN 2
|
||||
WHEN bc.container_size ILIKE '%20%' THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
END
|
||||
) AS "teuUsed"
|
||||
FROM freight.train_set_wagons tsw
|
||||
JOIN freight.wagon_booking_allocations wba
|
||||
ON wba.train_set_wagon_id = tsw.id AND wba.deleted_at IS NULL
|
||||
JOIN freight.wagon_allocation_container_items wci
|
||||
ON wci.wagon_booking_allocation_id = wba.id AND wci.deleted_at IS NULL
|
||||
LEFT JOIN freight.container_types ct ON ct.id = wci.container_type_id
|
||||
LEFT JOIN freight.booking_container bc ON bc.id = wci.booking_container_id
|
||||
LEFT JOIN freight.container_types bct ON bct.id = bc.container_type_id
|
||||
LEFT JOIN freight.wagons pw ON pw.id = tsw.physical_wagon_id
|
||||
WHERE tsw.train_set_id = (
|
||||
SELECT train_set_id FROM freight.train_schedules WHERE id = $1
|
||||
)
|
||||
AND tsw.deleted_at IS NULL
|
||||
GROUP BY tsw.id, tsw.sequence_no, pw.wagon_number
|
||||
HAVING SUM(
|
||||
CASE COALESCE(ct.size_ft, bct.size_ft)
|
||||
WHEN 40 THEN 2
|
||||
WHEN 20 THEN 1
|
||||
ELSE
|
||||
CASE
|
||||
WHEN bc.container_size ILIKE '%40%' THEN 2
|
||||
WHEN bc.container_size ILIKE '%20%' THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
END
|
||||
) > $2`,
|
||||
[scheduleId, MAX_TEU_SLOTS_PER_WAGON],
|
||||
);
|
||||
if (rows.length) {
|
||||
const labels = rows
|
||||
.map((r) => `wagon #${r.sequenceNo}${r.wagonNumber ? ` (${r.wagonNumber})` : ''}`)
|
||||
.join(', ');
|
||||
throw new BadRequestException(
|
||||
`These wagons exceed capacity (max 1×40ft or 2×20ft per wagon) — fix the container placement before confirming loading: ${labels}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private buildImportLoadListHtml(
|
||||
loadList: Awaited<ReturnType<TrainSchedulingService['generateImportLoadList']>>,
|
||||
logoImageUrl?: string | null,
|
||||
): string {
|
||||
const esc = (value: unknown) =>
|
||||
String(value ?? '-')
|
||||
.replace(/&/g, '&')
|
||||
@@ -3430,26 +3536,37 @@ export class TrainSchedulingService {
|
||||
const allocationRows = loadList.wagons
|
||||
.flatMap((wagon) => {
|
||||
const wagonCells = `<td>${esc(wagon.sequenceNo)}</td>
|
||||
<td>${esc(wagon.wagonNumber)}</td>`;
|
||||
<td>${esc(wagon.wagonNumber)}</td>
|
||||
<td>${esc(wagon.wagonType)}</td>
|
||||
<td class="num">${wagon.tareWeightTons == null ? '-' : esc(Number(wagon.tareWeightTons).toFixed(2))}</td>
|
||||
<td class="num">${wagon.equatedLengthM == null ? '-' : esc(Number(wagon.equatedLengthM).toFixed(3))}</td>
|
||||
<td>${esc(loadList.origin)}</td>
|
||||
<td>${esc(loadList.destination)}</td>`;
|
||||
// An empty wagon still runs in the consist, so it still gets a line — see
|
||||
// buildExportLoadListHtml.
|
||||
if (wagon.allocations.length === 0) {
|
||||
return [
|
||||
`<tr class="empty">
|
||||
${wagonCells}
|
||||
<td colspan="4">EMPTY — no cargo allocated</td>
|
||||
<td colspan="7">EMPTY — no cargo allocated</td>
|
||||
</tr>`,
|
||||
];
|
||||
}
|
||||
return wagon.allocations.map(
|
||||
(allocation) => {
|
||||
const companyName = (allocation.booking as unknown as { company?: { name?: string } } | undefined)?.company?.name ?? '-';
|
||||
const sealNumbers = (allocation.containerItems ?? [])
|
||||
.map((item) => item.sealNumber)
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
return `<tr>
|
||||
${wagonCells}
|
||||
<td>${esc(allocation.bookingReference ?? allocation.bookingId)}</td>
|
||||
<td>${esc(companyName)}</td>
|
||||
<td>${esc(allocation.loadType)}</td>
|
||||
<td>${esc(allocation.containerNumbers.length ? allocation.containerNumbers.join(', ') : '-')}</td>
|
||||
<td>${esc(sealNumbers || '-')}</td>
|
||||
<td></td>
|
||||
<td class="num">${esc(Number(allocation.allocatedWeightTons || 0).toFixed(3))}</td>
|
||||
</tr>`;
|
||||
},
|
||||
@@ -3477,6 +3594,7 @@ export class TrainSchedulingService {
|
||||
.tile { border: 1px solid #cbd5e1; padding: 10px; min-height: 58px; }
|
||||
.tile span { display: block; color: #64748b; font-size: 10px; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 5px; }
|
||||
.tile strong { font-size: 13px; }
|
||||
${logoImageCss()}
|
||||
.status { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; margin-top: 14px; }
|
||||
.step { border: 1px solid #cbd5e1; padding: 8px; font-size: 10px; text-align: center; min-height: 48px; }
|
||||
.done { background: #ecfdf5; border-color: #22c55e; color: #14532d; font-weight: 700; }
|
||||
@@ -3498,6 +3616,7 @@ export class TrainSchedulingService {
|
||||
<div class="doc">
|
||||
<div class="top">
|
||||
<div>
|
||||
${logoMarkup(logoImageUrl)}
|
||||
<div class="brand">Ethio-Djibouti Railway S.C.</div>
|
||||
<h1>Import Load List /<br />Marshalling Document</h1>
|
||||
<div class="subtitle">Djibouti-side gatepass, loading, and departure manifest</div>
|
||||
@@ -3538,15 +3657,22 @@ export class TrainSchedulingService {
|
||||
<tr>
|
||||
<th>Seq</th>
|
||||
<th>Wagon</th>
|
||||
<th>Wagon Type</th>
|
||||
<th class="num">Tare</th>
|
||||
<th class="num">Equated</th>
|
||||
<th>Departure Station</th>
|
||||
<th>Arrival Station</th>
|
||||
<th>Booking</th>
|
||||
<th>Company</th>
|
||||
<th>Load</th>
|
||||
<th>Container numbers</th>
|
||||
<th>Seal No</th>
|
||||
<th>Note</th>
|
||||
<th class="num">Weight T</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${allocationRows || '<tr><td colspan="6">No wagons on this train set.</td></tr>'}
|
||||
${allocationRows || '<tr><td colspan="14">No wagons on this train set.</td></tr>'}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -6881,6 +7007,7 @@ export class TrainSchedulingService {
|
||||
LEFT JOIN freight.yards dy ON dy.id = ts.destination_station_id
|
||||
WHERE ts.deleted_at IS NULL
|
||||
AND ts.status IN ('DRAFT', 'SCHEDULED')
|
||||
AND ts.shipping_line_company_id IS NULL
|
||||
AND ts.window_phase IS NOT NULL
|
||||
AND ts.window_phase NOT IN ('DONE', 'CLOSED_FOR_DAY')
|
||||
AND ts.scheduled_departure_date >= now()
|
||||
@@ -6937,6 +7064,7 @@ export class TrainSchedulingService {
|
||||
LEFT JOIN freight.yards dy ON dy.id = ts.destination_station_id
|
||||
WHERE ts.deleted_at IS NULL
|
||||
AND ts.status IN ('DRAFT', 'SCHEDULED')
|
||||
AND ts.shipping_line_company_id IS NULL
|
||||
AND ts.window_phase IS NOT NULL
|
||||
AND ts.window_phase NOT IN ('DONE', 'CLOSED_FOR_DAY')
|
||||
AND ts.scheduled_departure_date >= now()
|
||||
@@ -7046,6 +7174,8 @@ export class TrainSchedulingService {
|
||||
const schedules = await this.trainSchedulesRepository.findAll({
|
||||
where: {
|
||||
bookingWindowStatus: 'OPEN',
|
||||
// Dedicated shipping-line trains never surface to customer booking.
|
||||
shippingLineCompanyId: IsNull(),
|
||||
},
|
||||
relations: {
|
||||
trainSet: { locomotive: true, locomotives: { locomotive: true }, train: true },
|
||||
|
||||
Reference in New Issue
Block a user