mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
feat(intercity): a facility only handles the cargo its equipment can lift
Containers need a reach stacker or gantry, so only Indode, Modjo and Dire Dawa take them. Bulk needs far less and is handled at all five facilities. Having a facility was previously enough to load anything, so a container booking through Sebeta or Adama would have been accepted and then had nothing to lift it. - yard_facilities gains handles_container / handles_bulk, both defaulting true so a facility handles everything unless told otherwise; the seeder states the real capability. - The intercity gate now refuses cargo a facility cannot lift, saying which type, not just "no facility". canHandleFreight keeps that rule in the resolver so callers cannot get it subtly wrong. - The intercity list resolves each end against the booking's own freight type, so the view flags a container booking routed through a bulk-only yard while the train is still coming rather than when the load is refused. Import/export untouched — the gate is still DOMESTIC-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A facility handles what its equipment can handle. Containers need a reach
|
||||||
|
* stacker or gantry, so only Indode, Modjo and Dire Dawa take them; bulk needs
|
||||||
|
* far less, so all five facilities load and unload it.
|
||||||
|
*
|
||||||
|
* Both default true — a facility handles everything unless someone says
|
||||||
|
* otherwise, which keeps existing rows working and makes the seeder the place
|
||||||
|
* where the real capability is stated.
|
||||||
|
*/
|
||||||
|
export class YardFacilityFreightTypes2320000000000 implements MigrationInterface {
|
||||||
|
name = 'YardFacilityFreightTypes2320000000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.yard_facilities
|
||||||
|
ADD COLUMN IF NOT EXISTS handles_container boolean NOT NULL DEFAULT true,
|
||||||
|
ADD COLUMN IF NOT EXISTS handles_bulk boolean NOT NULL DEFAULT true
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
ALTER TABLE freight.yard_facilities
|
||||||
|
DROP COLUMN IF EXISTS handles_container,
|
||||||
|
DROP COLUMN IF EXISTS handles_bulk
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,17 @@ export class YardFacility extends BaseEntity {
|
|||||||
@Column({ name: 'has_warehouse', type: 'boolean', default: false })
|
@Column({ name: 'has_warehouse', type: 'boolean', default: false })
|
||||||
hasWarehouse!: boolean;
|
hasWarehouse!: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Containers need a reach stacker or gantry, so only the equipped facilities
|
||||||
|
* (Indode, Modjo, Dire Dawa) take them. Bulk needs far less and is handled
|
||||||
|
* everywhere.
|
||||||
|
*/
|
||||||
|
@Column({ name: 'handles_container', type: 'boolean', default: true })
|
||||||
|
handlesContainer!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: 'handles_bulk', type: 'boolean', default: true })
|
||||||
|
handlesBulk!: boolean;
|
||||||
|
|
||||||
@Column({ name: 'equipment_notes', type: 'text', nullable: true })
|
@Column({ name: 'equipment_notes', type: 'text', nullable: true })
|
||||||
equipmentNotes?: string | null;
|
equipmentNotes?: string | null;
|
||||||
|
|
||||||
|
|||||||
@@ -10,80 +10,91 @@ export interface YardFacilityInfo {
|
|||||||
hasFacility: boolean;
|
hasFacility: boolean;
|
||||||
/** The facility stores cargo — enables the warehouse flow (storage, demurrage). */
|
/** The facility stores cargo — enables the warehouse flow (storage, demurrage). */
|
||||||
hasWarehouse: boolean;
|
hasWarehouse: boolean;
|
||||||
|
/** Containers need a reach stacker/gantry — not every facility has one. */
|
||||||
|
handlesContainer: boolean;
|
||||||
|
handlesBulk: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Which yards can handle cargo, and how.
|
* Which yards can handle cargo, and what kind.
|
||||||
*
|
*
|
||||||
* A yard is a load/unload point when `yards.has_facility` is set; the matching
|
* A yard is a load/unload point when `yards.has_facility` is set; the matching
|
||||||
* `yard_facilities` record says whether it also stores cargo. Facilities without a
|
* `yard_facilities` record says what it can actually do — whether it stores cargo
|
||||||
* warehouse move cargo on and off the train and nothing more — no storage, no
|
* (storage/demurrage), and which freight types its equipment can lift. Containers
|
||||||
* demurrage. This is the single resolver the journey and handling flows use, so
|
* need a reach stacker or gantry, so only Indode, Modjo and Dire Dawa take them;
|
||||||
* they can't drift on what a facility is.
|
* bulk is handled at all five.
|
||||||
|
*
|
||||||
|
* This is the single resolver the journey and handling flows use, so they can't
|
||||||
|
* drift on what a facility is or what it can lift.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class YardFacilitiesService {
|
export class YardFacilitiesService {
|
||||||
constructor(private readonly dataSource: DataSource) {}
|
constructor(private readonly dataSource: DataSource) {}
|
||||||
|
|
||||||
/** Resolve a yard's handling capability. Null when the yard doesn't exist. */
|
private readonly SELECT = `
|
||||||
async facilityForYard(yardId: string): Promise<YardFacilityInfo | null> {
|
SELECT y.id AS "yardId",
|
||||||
const [row]: Array<{
|
y.code AS "yardCode",
|
||||||
yardId: string;
|
y.label AS "yardLabel",
|
||||||
yardCode: string | null;
|
y.has_facility AS "hasFacility",
|
||||||
yardLabel: string | null;
|
f.has_warehouse AS "hasWarehouse",
|
||||||
hasFacility: boolean;
|
f.handles_container AS "handlesContainer",
|
||||||
hasWarehouse: boolean | null;
|
f.handles_bulk AS "handlesBulk"
|
||||||
}> = await this.dataSource.query(
|
FROM freight.yards y
|
||||||
`SELECT y.id AS "yardId",
|
LEFT JOIN freight.yard_facilities f
|
||||||
y.code AS "yardCode",
|
ON f.yard_id = y.id AND f.deleted_at IS NULL AND f.is_active = true`;
|
||||||
y.label AS "yardLabel",
|
|
||||||
y.has_facility AS "hasFacility",
|
private toInfo(row: {
|
||||||
f.has_warehouse AS "hasWarehouse"
|
yardId: string;
|
||||||
FROM freight.yards y
|
yardCode: string | null;
|
||||||
LEFT JOIN freight.yard_facilities f
|
yardLabel: string | null;
|
||||||
ON f.yard_id = y.id AND f.deleted_at IS NULL AND f.is_active = true
|
hasFacility: boolean;
|
||||||
WHERE y.id = $1 AND y.deleted_at IS NULL`,
|
hasWarehouse: boolean | null;
|
||||||
[yardId],
|
handlesContainer: boolean | null;
|
||||||
);
|
handlesBulk: boolean | null;
|
||||||
if (!row) return null;
|
}): YardFacilityInfo {
|
||||||
|
// No facility record means no capability, whatever the flag says.
|
||||||
|
const hasFacility = Boolean(row.hasFacility);
|
||||||
return {
|
return {
|
||||||
yardId: row.yardId,
|
yardId: row.yardId,
|
||||||
yardCode: row.yardCode,
|
yardCode: row.yardCode,
|
||||||
yardLabel: row.yardLabel,
|
yardLabel: row.yardLabel,
|
||||||
hasFacility: Boolean(row.hasFacility),
|
hasFacility,
|
||||||
// No facility record means no warehouse, whatever the flag says.
|
hasWarehouse: hasFacility && Boolean(row.hasWarehouse),
|
||||||
hasWarehouse: Boolean(row.hasFacility) && Boolean(row.hasWarehouse),
|
handlesContainer: hasFacility && Boolean(row.handlesContainer),
|
||||||
|
handlesBulk: hasFacility && Boolean(row.handlesBulk),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolve a yard's handling capability. Null when the yard doesn't exist. */
|
||||||
|
async facilityForYard(yardId: string): Promise<YardFacilityInfo | null> {
|
||||||
|
const [row] = await this.dataSource.query(
|
||||||
|
`${this.SELECT} WHERE y.id = $1 AND y.deleted_at IS NULL`,
|
||||||
|
[yardId],
|
||||||
|
);
|
||||||
|
return row ? this.toInfo(row) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Every yard that can load/unload, for pickers and the intercity queues. */
|
/** Every yard that can load/unload, for pickers and the intercity queues. */
|
||||||
async listFacilityYards(): Promise<YardFacilityInfo[]> {
|
async listFacilityYards(): Promise<YardFacilityInfo[]> {
|
||||||
const rows: Array<{
|
const rows = await this.dataSource.query(
|
||||||
yardId: string;
|
`${this.SELECT}
|
||||||
yardCode: string | null;
|
WHERE y.deleted_at IS NULL AND y.is_active = true AND y.has_facility = true
|
||||||
yardLabel: string | null;
|
|
||||||
hasFacility: boolean;
|
|
||||||
hasWarehouse: boolean | null;
|
|
||||||
}> = await this.dataSource.query(
|
|
||||||
`SELECT y.id AS "yardId",
|
|
||||||
y.code AS "yardCode",
|
|
||||||
y.label AS "yardLabel",
|
|
||||||
y.has_facility AS "hasFacility",
|
|
||||||
f.has_warehouse AS "hasWarehouse"
|
|
||||||
FROM freight.yards y
|
|
||||||
LEFT JOIN freight.yard_facilities f
|
|
||||||
ON f.yard_id = y.id AND f.deleted_at IS NULL AND f.is_active = true
|
|
||||||
WHERE y.deleted_at IS NULL
|
|
||||||
AND y.is_active = true
|
|
||||||
AND y.has_facility = true
|
|
||||||
ORDER BY y.display_order ASC, y.label ASC`,
|
ORDER BY y.display_order ASC, y.label ASC`,
|
||||||
);
|
);
|
||||||
return rows.map((r) => ({
|
return rows.map((r: Parameters<typeof this.toInfo>[0]) => this.toInfo(r));
|
||||||
yardId: r.yardId,
|
}
|
||||||
yardCode: r.yardCode,
|
|
||||||
yardLabel: r.yardLabel,
|
/**
|
||||||
hasFacility: true,
|
* Can this facility lift this cargo? Keeps the freight-type rule in one place
|
||||||
hasWarehouse: Boolean(r.hasWarehouse),
|
* so callers can't get it subtly wrong.
|
||||||
}));
|
*/
|
||||||
|
canHandleFreight(
|
||||||
|
facility: YardFacilityInfo | null,
|
||||||
|
freightType: string | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
if (!facility?.hasFacility) return false;
|
||||||
|
return String(freightType).toUpperCase() === 'CONTAINER'
|
||||||
|
? facility.handlesContainer
|
||||||
|
: facility.handlesBulk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,10 +352,19 @@ export class BookingJourneyService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (booking.tradeDirection !== 'DOMESTIC') return;
|
if (booking.tradeDirection !== 'DOMESTIC') return;
|
||||||
const facility = await this.yardFacilities.facilityForYard(yardId);
|
const facility = await this.yardFacilities.facilityForYard(yardId);
|
||||||
|
const where = side === 'origin' ? 'loaded at its origin' : 'unloaded at its destination';
|
||||||
|
|
||||||
if (!facility?.hasFacility) {
|
if (!facility?.hasFacility) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`${facility?.yardLabel ?? 'This yard'} has no load/unload facility — an intercity booking cannot be ` +
|
`${facility?.yardLabel ?? 'This yard'} has no load/unload facility — an intercity booking cannot be ${where} here.`,
|
||||||
`${side === 'origin' ? 'loaded at its origin' : 'unloaded at its destination'} here.`,
|
);
|
||||||
|
}
|
||||||
|
// A facility only handles what its equipment can lift: containers need a
|
||||||
|
// reach stacker/gantry, bulk does not.
|
||||||
|
if (!this.yardFacilities.canHandleFreight(facility, booking.freightType)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`${facility.yardLabel ?? 'This yard'} does not handle ${String(booking.freightType).toLowerCase()} cargo — ` +
|
||||||
|
`an intercity booking cannot be ${where} here.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,10 +67,18 @@ export class IntercityService {
|
|||||||
ts.status AS "scheduleStatus",
|
ts.status AS "scheduleStatus",
|
||||||
oy.id AS "originYardId",
|
oy.id AS "originYardId",
|
||||||
COALESCE(oy.label, oy.code) AS "origin",
|
COALESCE(oy.label, oy.code) AS "origin",
|
||||||
oy.has_facility AS "originHasFacility",
|
-- Can that end actually handle THIS booking's cargo? A container
|
||||||
|
-- booking needs a facility with a stacker; bulk needs any facility.
|
||||||
|
(oy.has_facility AND COALESCE(
|
||||||
|
CASE WHEN b.freight_type = 'CONTAINER'
|
||||||
|
THEN ofac.handles_container ELSE ofac.handles_bulk END, false))
|
||||||
|
AS "originHasFacility",
|
||||||
dy.id AS "destinationYardId",
|
dy.id AS "destinationYardId",
|
||||||
COALESCE(dy.label, dy.code) AS "destination",
|
COALESCE(dy.label, dy.code) AS "destination",
|
||||||
dy.has_facility AS "destinationHasFacility",
|
(dy.has_facility AND COALESCE(
|
||||||
|
CASE WHEN b.freight_type = 'CONTAINER'
|
||||||
|
THEN dfac.handles_container ELSE dfac.handles_bulk END, false))
|
||||||
|
AS "destinationHasFacility",
|
||||||
-- Where the train actually is, so the operator knows if the cargo
|
-- Where the train actually is, so the operator knows if the cargo
|
||||||
-- can be worked right now.
|
-- can be worked right now.
|
||||||
cp.yard_id AS "trainAtYardId",
|
cp.yard_id AS "trainAtYardId",
|
||||||
@@ -80,6 +88,10 @@ export class IntercityService {
|
|||||||
LEFT JOIN freight.companies company ON company.id = b.company_id
|
LEFT JOIN freight.companies company ON company.id = b.company_id
|
||||||
LEFT JOIN freight.yards oy ON oy.id = b.origin_yard_id
|
LEFT JOIN freight.yards oy ON oy.id = b.origin_yard_id
|
||||||
LEFT JOIN freight.yards dy ON dy.id = b.destination_yard_id
|
LEFT JOIN freight.yards dy ON dy.id = b.destination_yard_id
|
||||||
|
LEFT JOIN freight.yard_facilities ofac
|
||||||
|
ON ofac.yard_id = oy.id AND ofac.deleted_at IS NULL AND ofac.is_active = true
|
||||||
|
LEFT JOIN freight.yard_facilities dfac
|
||||||
|
ON dfac.yard_id = dy.id AND dfac.deleted_at IS NULL AND dfac.is_active = true
|
||||||
LEFT JOIN freight.train_schedules ts
|
LEFT JOIN freight.train_schedules ts
|
||||||
ON ts.id = b.train_schedule_id AND ts.deleted_at IS NULL
|
ON ts.id = b.train_schedule_id AND ts.deleted_at IS NULL
|
||||||
LEFT JOIN LATERAL (
|
LEFT JOIN LATERAL (
|
||||||
|
|||||||
@@ -16,12 +16,19 @@ import { DataSource } from 'typeorm';
|
|||||||
* Djibouti and `NEGAD_FY_BCC` in Ethiopia, currently inactive) and it is not yet
|
* Djibouti and `NEGAD_FY_BCC` in Ethiopia, currently inactive) and it is not yet
|
||||||
* settled which is the intercity facility.
|
* settled which is the intercity facility.
|
||||||
*/
|
*/
|
||||||
const FACILITY_YARDS: Array<{ code: string; facility: string; hasWarehouse: boolean }> = [
|
const FACILITY_YARDS: Array<{
|
||||||
{ code: 'KALITY', facility: 'Indode', hasWarehouse: true },
|
code: string;
|
||||||
{ code: 'LEGACY_DEST', facility: 'Sebeta', hasWarehouse: false },
|
facility: string;
|
||||||
{ code: 'MOJO', facility: 'Modjo', hasWarehouse: false },
|
hasWarehouse: boolean;
|
||||||
{ code: 'ADAMA', facility: 'Adama', hasWarehouse: false },
|
handlesContainer: boolean;
|
||||||
{ code: 'DIRE_DAWA', facility: 'Dire Dawa', hasWarehouse: false },
|
}> = [
|
||||||
|
// Containers need a reach stacker or gantry — only these three are equipped.
|
||||||
|
// Bulk needs far less, so every facility handles it.
|
||||||
|
{ code: 'KALITY', facility: 'Indode', hasWarehouse: true, handlesContainer: true },
|
||||||
|
{ code: 'MOJO', facility: 'Modjo', hasWarehouse: false, handlesContainer: true },
|
||||||
|
{ code: 'DIRE_DAWA', facility: 'Dire Dawa', hasWarehouse: false, handlesContainer: true },
|
||||||
|
{ code: 'LEGACY_DEST', facility: 'Sebeta', hasWarehouse: false, handlesContainer: false },
|
||||||
|
{ code: 'ADAMA', facility: 'Adama', hasWarehouse: false, handlesContainer: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -35,7 +42,7 @@ export class YardFacilitiesSeeder {
|
|||||||
* yards — a missing code is logged and skipped rather than invented.
|
* yards — a missing code is logged and skipped rather than invented.
|
||||||
*/
|
*/
|
||||||
async run(): Promise<void> {
|
async run(): Promise<void> {
|
||||||
for (const { code, facility, hasWarehouse } of FACILITY_YARDS) {
|
for (const { code, facility, hasWarehouse, handlesContainer } of FACILITY_YARDS) {
|
||||||
const [yard]: Array<{ id: string }> = await this.dataSource.query(
|
const [yard]: Array<{ id: string }> = await this.dataSource.query(
|
||||||
`SELECT id FROM freight.yards WHERE code = $1 AND deleted_at IS NULL`,
|
`SELECT id FROM freight.yards WHERE code = $1 AND deleted_at IS NULL`,
|
||||||
[code],
|
[code],
|
||||||
@@ -53,11 +60,15 @@ export class YardFacilitiesSeeder {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await this.dataSource.query(
|
await this.dataSource.query(
|
||||||
`INSERT INTO freight.yard_facilities (yard_id, has_warehouse, equipment_notes)
|
`INSERT INTO freight.yard_facilities
|
||||||
VALUES ($1, $2, $3)
|
(yard_id, has_warehouse, handles_container, handles_bulk, equipment_notes)
|
||||||
|
VALUES ($1, $2, $3, true, $4)
|
||||||
ON CONFLICT (yard_id) WHERE deleted_at IS NULL
|
ON CONFLICT (yard_id) WHERE deleted_at IS NULL
|
||||||
DO UPDATE SET has_warehouse = EXCLUDED.has_warehouse, updated_at = NOW()`,
|
DO UPDATE SET has_warehouse = EXCLUDED.has_warehouse,
|
||||||
[yard.id, hasWarehouse, `${facility} load/unload facility`],
|
handles_container = EXCLUDED.handles_container,
|
||||||
|
handles_bulk = EXCLUDED.handles_bulk,
|
||||||
|
updated_at = NOW()`,
|
||||||
|
[yard.id, hasWarehouse, handlesContainer, `${facility} load/unload facility`],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
|
|||||||
@@ -40,16 +40,29 @@ const isWaiting = (r: IntercityRideAlongRow) =>
|
|||||||
const isRiding = (r: IntercityRideAlongRow) => r.status === "IN_TRANSIT";
|
const isRiding = (r: IntercityRideAlongRow) => r.status === "IN_TRANSIT";
|
||||||
const isDone = (r: IntercityRideAlongRow) => r.status === "COMPLETED";
|
const isDone = (r: IntercityRideAlongRow) => r.status === "COMPLETED";
|
||||||
|
|
||||||
/** Yards with no equipment can never load/unload — surface it before the train arrives. */
|
/**
|
||||||
function FacilityCell({ yard, has }: { yard: string | null; has: boolean | null }) {
|
* A yard that can't handle THIS booking's cargo can never work it — surface that
|
||||||
|
* while the train is still coming, not when the load is refused. Containers need
|
||||||
|
* a facility with a stacker (Indode, Modjo, Dire Dawa); bulk is handled at all of
|
||||||
|
* them.
|
||||||
|
*/
|
||||||
|
function FacilityCell({
|
||||||
|
yard,
|
||||||
|
has,
|
||||||
|
freightType,
|
||||||
|
}: {
|
||||||
|
yard: string | null;
|
||||||
|
has: boolean | null;
|
||||||
|
freightType: string | null;
|
||||||
|
}) {
|
||||||
if (!yard) return <Text size="sm">—</Text>;
|
if (!yard) return <Text size="sm">—</Text>;
|
||||||
if (has) return <Text size="sm">{yard}</Text>;
|
if (has) return <Text size="sm">{yard}</Text>;
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
label="This yard has no load/unload facility — cargo cannot be handled here"
|
label={`${yard} cannot handle ${(freightType ?? "this").toLowerCase()} cargo — no facility here, or no equipment for it`}
|
||||||
withArrow
|
withArrow
|
||||||
multiline
|
multiline
|
||||||
w={240}
|
w={260}
|
||||||
>
|
>
|
||||||
<Group gap={4} wrap="nowrap">
|
<Group gap={4} wrap="nowrap">
|
||||||
<AlertTriangle size={13} color="var(--mantine-color-red-6)" />
|
<AlertTriangle size={13} color="var(--mantine-color-red-6)" />
|
||||||
@@ -95,7 +108,7 @@ function Rows({ rows }: { rows: IntercityRideAlongRow[] }) {
|
|||||||
<Table.Td>{r.customer ?? "—"}</Table.Td>
|
<Table.Td>{r.customer ?? "—"}</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group gap={6} wrap="nowrap">
|
<Group gap={6} wrap="nowrap">
|
||||||
<FacilityCell yard={r.origin} has={r.originHasFacility} />
|
<FacilityCell yard={r.origin} has={r.originHasFacility} freightType={r.freightType} />
|
||||||
{atOrigin(r) && isWaiting(r) && (
|
{atOrigin(r) && isWaiting(r) && (
|
||||||
<Badge size="xs" color="edr-green" variant="light">
|
<Badge size="xs" color="edr-green" variant="light">
|
||||||
train here
|
train here
|
||||||
@@ -105,7 +118,7 @@ function Rows({ rows }: { rows: IntercityRideAlongRow[] }) {
|
|||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group gap={6} wrap="nowrap">
|
<Group gap={6} wrap="nowrap">
|
||||||
<FacilityCell yard={r.destination} has={r.destinationHasFacility} />
|
<FacilityCell yard={r.destination} has={r.destinationHasFacility} freightType={r.freightType} />
|
||||||
{atDestination(r) && isRiding(r) && (
|
{atDestination(r) && isRiding(r) && (
|
||||||
<Badge size="xs" color="edr-green" variant="light">
|
<Badge size="xs" color="edr-green" variant="light">
|
||||||
train here
|
train here
|
||||||
@@ -215,7 +228,7 @@ export default function IntercityPage() {
|
|||||||
<Stat icon={<Warehouse size={18} />} label="Completed" value={done.length} />
|
<Stat icon={<Warehouse size={18} />} label="Completed" value={done.length} />
|
||||||
<Stat
|
<Stat
|
||||||
icon={<AlertTriangle size={18} />}
|
icon={<AlertTriangle size={18} />}
|
||||||
label="No facility"
|
label="Cannot handle"
|
||||||
value={blocked.length}
|
value={blocked.length}
|
||||||
color={blocked.length > 0 ? "red" : undefined}
|
color={blocked.length > 0 ? "red" : undefined}
|
||||||
/>
|
/>
|
||||||
@@ -229,8 +242,9 @@ export default function IntercityPage() {
|
|||||||
title={`${blocked.length} booking${blocked.length === 1 ? "" : "s"} cannot be handled`}
|
title={`${blocked.length} booking${blocked.length === 1 ? "" : "s"} cannot be handled`}
|
||||||
mb="md"
|
mb="md"
|
||||||
>
|
>
|
||||||
Their origin or destination yard has no load/unload facility. Mark the yard as
|
Their origin or destination yard cannot handle that cargo — no facility, or no
|
||||||
a facility in Configuration → Yards, or the cargo can never be worked there.
|
equipment for it. Containers need Indode, Modjo or Dire Dawa; bulk is handled at
|
||||||
|
any facility. Adjust the yard in Configuration → Yards.
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user