mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +00:00
Merge pull request #1106 from Tria-plc/edrmiles
feat(warehouses): filters, pagination and charts on container returns
This commit is contained in:
@@ -5,6 +5,7 @@ import { resolve } from 'path';
|
|||||||
config({ path: resolve(__dirname, '../../.env') });
|
config({ path: resolve(__dirname, '../../.env') });
|
||||||
|
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
import { AppModule } from '../app.module';
|
import { AppModule } from '../app.module';
|
||||||
import { Batch14TestDataSeeder } from '../seed/batch1-4-test-data.seeder';
|
import { Batch14TestDataSeeder } from '../seed/batch1-4-test-data.seeder';
|
||||||
import { Batch5TestDataSeeder } from '../seed/batch5-test-data.seeder';
|
import { Batch5TestDataSeeder } from '../seed/batch5-test-data.seeder';
|
||||||
@@ -14,19 +15,33 @@ import { IndodeFacilitySeeder } from '../seed/indode-facility.seeder';
|
|||||||
import { PricingDataSeeder } from '../seed/pricing-data.seeder';
|
import { PricingDataSeeder } from '../seed/pricing-data.seeder';
|
||||||
import { WarehouseDemoSeeder } from '../seed/warehouse-demo.seeder';
|
import { WarehouseDemoSeeder } from '../seed/warehouse-demo.seeder';
|
||||||
|
|
||||||
|
/** Demo data only — refuse to run against anything but a local dev database. */
|
||||||
|
function assertLocalhost() {
|
||||||
|
const host = process.env.DB_HOST ?? 'localhost';
|
||||||
|
if (host !== 'localhost' && host !== '127.0.0.1') {
|
||||||
|
console.error(`Refusing to seed demo data: DB_HOST is "${host}", not localhost.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
assertLocalhost();
|
||||||
|
|
||||||
const app = await NestFactory.createApplicationContext(AppModule, {
|
const app = await NestFactory.createApplicationContext(AppModule, {
|
||||||
logger: ['error', 'warn', 'log'],
|
logger: ['error', 'warn', 'log'],
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await app.get(PricingDataSeeder).run();
|
// Demo seeders are intentionally not AppModule providers (they'd run on every
|
||||||
await app.get(IndodeFacilitySeeder).run();
|
// boot), so construct them against the app's DataSource instead of via DI.
|
||||||
await app.get(Batch14TestDataSeeder).run();
|
const dataSource = app.get(DataSource);
|
||||||
await app.get(Batch5TestDataSeeder).run();
|
await new PricingDataSeeder(dataSource).run();
|
||||||
await app.get(Batch7TestDataSeeder).run();
|
await new IndodeFacilitySeeder(dataSource).run();
|
||||||
await app.get(Batch8TestDataSeeder).run();
|
await new Batch14TestDataSeeder(dataSource).run();
|
||||||
await app.get(WarehouseDemoSeeder).run();
|
await new Batch5TestDataSeeder(dataSource).run();
|
||||||
|
await new Batch7TestDataSeeder(dataSource).run();
|
||||||
|
await new Batch8TestDataSeeder(dataSource).run();
|
||||||
|
await new WarehouseDemoSeeder(dataSource).run();
|
||||||
|
|
||||||
console.log('Warehouse demo data seeded.');
|
console.log('Warehouse demo data seeded.');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
import { Booking } from '../modules/bookings/entities/booking.entity';
|
import { Booking } from '../modules/bookings/entities/booking.entity';
|
||||||
|
import { CustomerTruckAssignment } from '../modules/bookings/entities/customer-truck-assignment.entity';
|
||||||
import { CargoType } from '../modules/rule-engine/entities/cargo-type.entity';
|
import { CargoType } from '../modules/rule-engine/entities/cargo-type.entity';
|
||||||
import { CompanyProfile } from '../modules/companies/entities/company-profile.entity';
|
import { CompanyProfile } from '../modules/companies/entities/company-profile.entity';
|
||||||
|
import { EmptyContainerReturn } from '../modules/import-operations/entities/empty-container-return.entity';
|
||||||
import { Locomotive } from '../modules/locomotives/entities/locomotive.entity';
|
import { Locomotive } from '../modules/locomotives/entities/locomotive.entity';
|
||||||
import { ServiceType } from '../modules/rule-engine/entities/service-type.entity';
|
import { ServiceType } from '../modules/rule-engine/entities/service-type.entity';
|
||||||
import { Yard } from '../modules/rule-engine/entities/yard.entity';
|
import { Yard } from '../modules/rule-engine/entities/yard.entity';
|
||||||
@@ -23,6 +25,8 @@ import { WarehouseZone } from '../modules/warehouses/entities/warehouse-zone.ent
|
|||||||
* Import → Arrive Queue : an ARRIVED import train with IN_TRANSIT bookings (no inventory)
|
* Import → Arrive Queue : an ARRIVED import train with IN_TRANSIT bookings (no inventory)
|
||||||
* Import → Unloaded Queue : UNLOADED import inventory
|
* Import → Unloaded Queue : UNLOADED import inventory
|
||||||
* Import → Dispatch Queue : READY_FOR_PICKUP import inventory (PASSED)
|
* Import → Dispatch Queue : READY_FOR_PICKUP import inventory (PASSED)
|
||||||
|
* Import → Import Trucks : a customer self-haul truck assigned to an unloaded booking
|
||||||
|
* Import → Container Returns : empty container returns at two different statuses
|
||||||
*
|
*
|
||||||
* Idempotent: guarded on a sentinel booking reference. Uses dedicated WH-DEMO-* references so it
|
* Idempotent: guarded on a sentinel booking reference. Uses dedicated WH-DEMO-* references so it
|
||||||
* never collides with other seeders. To repopulate after items are walked through their lifecycle,
|
* never collides with other seeders. To repopulate after items are walked through their lifecycle,
|
||||||
@@ -166,16 +170,19 @@ export class WarehouseDemoSeeder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4) Import Unloaded Queue — UNLOADED import inventory (not inspected, not stored).
|
// 4) Import Unloaded Queue — UNLOADED import inventory (not inspected, not stored).
|
||||||
|
let firstUnloadedBooking: Booking | null = null;
|
||||||
for (let i = 1; i <= 3; i++) {
|
for (let i = 1; i <= 3; i++) {
|
||||||
const b = await makeBooking(`WH-DEMO-UNL-${i}`, 'IMPORT', 'IN_TRANSIT', 5000 + i * 500, i);
|
const b = await makeBooking(`WH-DEMO-UNL-${i}`, 'IMPORT', 'IN_TRANSIT', 5000 + i * 500, i);
|
||||||
await makeInventory(b, 'UNLOADED', 5000 + i * 500, {
|
await makeInventory(b, 'UNLOADED', 5000 + i * 500, {
|
||||||
arrivedAt: ago(90),
|
arrivedAt: ago(90),
|
||||||
unloadedAt: ago(45),
|
unloadedAt: ago(45),
|
||||||
});
|
});
|
||||||
|
firstUnloadedBooking ??= b;
|
||||||
created++;
|
created++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5) Import Dispatch Queue — READY_FOR_PICKUP import inventory (inspection PASSED).
|
// 5) Import Dispatch Queue — READY_FOR_PICKUP import inventory (inspection PASSED).
|
||||||
|
let firstPickupBooking: Booking | null = null;
|
||||||
for (let i = 1; i <= 3; i++) {
|
for (let i = 1; i <= 3; i++) {
|
||||||
const b = await makeBooking(`WH-DEMO-PKR-${i}`, 'IMPORT', 'IN_TRANSIT', 5500 + i * 500, i);
|
const b = await makeBooking(`WH-DEMO-PKR-${i}`, 'IMPORT', 'IN_TRANSIT', 5500 + i * 500, i);
|
||||||
await makeInventory(b, 'READY_FOR_PICKUP', 5500 + i * 500, {
|
await makeInventory(b, 'READY_FOR_PICKUP', 5500 + i * 500, {
|
||||||
@@ -185,6 +192,50 @@ export class WarehouseDemoSeeder {
|
|||||||
inspectedAt: ago(120),
|
inspectedAt: ago(120),
|
||||||
readyForPickupAt: ago(60),
|
readyForPickupAt: ago(60),
|
||||||
});
|
});
|
||||||
|
firstPickupBooking ??= b;
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) Import Trucks / booking Trucks tab — a customer self-haul truck on the unloaded booking.
|
||||||
|
if (firstUnloadedBooking) {
|
||||||
|
await this.dataSource.getRepository(CustomerTruckAssignment).save(
|
||||||
|
this.dataSource.getRepository(CustomerTruckAssignment).create({
|
||||||
|
bookingId: firstUnloadedBooking.id,
|
||||||
|
plateNumber: 'WH-DEMO-3210',
|
||||||
|
driverName: 'Demo Driver',
|
||||||
|
truckType: 'FLATBED',
|
||||||
|
assignedAt: ago(80),
|
||||||
|
arrivedAt: ago(50),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
created++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7) Container Returns — two empty returns at different stages of the return workflow.
|
||||||
|
if (firstPickupBooking) {
|
||||||
|
const returnRepo = this.dataSource.getRepository(EmptyContainerReturn);
|
||||||
|
await returnRepo.save(
|
||||||
|
returnRepo.create({
|
||||||
|
containerNumber: 'WHDU1234561',
|
||||||
|
bookingId: firstPickupBooking.id,
|
||||||
|
returnDate: ago(20),
|
||||||
|
facility: 'Indode',
|
||||||
|
status: 'RETURNED',
|
||||||
|
returnedBy: 'CUSTOMER',
|
||||||
|
statusHistory: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await returnRepo.save(
|
||||||
|
returnRepo.create({
|
||||||
|
containerNumber: 'WHDU1234562',
|
||||||
|
bookingId: firstPickupBooking.id,
|
||||||
|
returnDate: ago(90),
|
||||||
|
facility: 'Indode',
|
||||||
|
status: 'DOCUMENTATION_CLEARED',
|
||||||
|
returnedBy: 'CUSTOMER',
|
||||||
|
statusHistory: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
created++;
|
created++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
|
Card,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
Modal,
|
Modal,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
|
SimpleGrid,
|
||||||
Stack,
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
Text,
|
Text,
|
||||||
@@ -18,16 +20,23 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { ChevronDown, ChevronRight, History } from "lucide-react";
|
import { ChevronDown, ChevronRight, History } from "lucide-react";
|
||||||
|
import { DataTable, type ColumnDef } from "@edr/ui-common";
|
||||||
|
|
||||||
import { PageContainer, PageHeader } from "@/components/page";
|
import { PageContainer, PageHeader } from "@/components/page";
|
||||||
|
import ListControls from "@/components/common/ListControls";
|
||||||
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
||||||
import { useListControls } from "@/hooks/useListControls";
|
import { OverviewHorizontalBarChart } from "@/components/overview/OverviewHorizontalBarChart";
|
||||||
|
import { OverviewStackedBarChart } from "@/components/overview/OverviewStackedBarChart";
|
||||||
|
import { useListControls, toDayString } from "@/hooks/useListControls";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useWarehouseYards, useWarehouseZones } from "@/hooks/useWarehouses";
|
import { useWarehouseYards, useWarehouseZones } from "@/hooks/useWarehouses";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import { warehouseService } from "@/services/warehouse.service";
|
import { warehouseService } from "@/services/warehouse.service";
|
||||||
import { importOperationsService } from "@/services/importOperations.service";
|
import { importOperationsService } from "@/services/importOperations.service";
|
||||||
import type { EmptyContainerReturnStatus } from "@/types/importOperations";
|
import type {
|
||||||
|
EmptyContainerReturn,
|
||||||
|
EmptyContainerReturnStatus,
|
||||||
|
} from "@/types/importOperations";
|
||||||
|
|
||||||
type ReturnType = "all" | "edr" | "customer";
|
type ReturnType = "all" | "edr" | "customer";
|
||||||
|
|
||||||
@@ -51,6 +60,13 @@ const RETURN_STATUS_LABEL: Record<EmptyContainerReturnStatus, string> = {
|
|||||||
COMPLETED: "Completed",
|
COMPLETED: "Completed",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fixed series colors (colors follow the entity, never the rank) — pair
|
||||||
|
// validated for CVD separation + surface contrast.
|
||||||
|
const RETURNED_BY_SERIES = [
|
||||||
|
{ key: "edr", label: "EDR Last Mile", color: "#0d9488" },
|
||||||
|
{ key: "customer", label: "Customer Self-Haul", color: "#b45309" },
|
||||||
|
];
|
||||||
|
|
||||||
interface ContainerReturnRow {
|
interface ContainerReturnRow {
|
||||||
key: string;
|
key: string;
|
||||||
containerNumber: string;
|
containerNumber: string;
|
||||||
@@ -186,10 +202,47 @@ export default function ContainerReturnsPage() {
|
|||||||
enabled: bookingIds.length > 0 && !queueLoading,
|
enabled: bookingIds.length > 0 && !queueLoading,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [statusFilter, setStatusFilter] = useState<string | null>(null);
|
||||||
|
|
||||||
const filteredReturnedContainers = useMemo(() => {
|
const filteredReturnedContainers = useMemo(() => {
|
||||||
if (filterType === "all") return returnedContainers;
|
let rows = returnedContainers as EmptyContainerReturn[];
|
||||||
return returnedContainers.filter((ret: any) => ret.returnedBy === filterType.toUpperCase());
|
if (filterType !== "all") {
|
||||||
}, [returnedContainers, filterType]);
|
rows = rows.filter((ret) => ret.returnedBy === filterType.toUpperCase());
|
||||||
|
}
|
||||||
|
if (statusFilter) {
|
||||||
|
rows = rows.filter((ret) => ret.status === statusFilter);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}, [returnedContainers, filterType, statusFilter]);
|
||||||
|
|
||||||
|
const returnedControls = useListControls(filteredReturnedContainers, {
|
||||||
|
dateKey: "returnDate",
|
||||||
|
searchValue: (ret) =>
|
||||||
|
`${ret.containerNumber} ${ret.facility ?? ""} ${ret.yard ?? ""} ${ret.condition ?? ""}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Charts read the filtered set, so the controls above drive them too.
|
||||||
|
const returnsPerDay = useMemo(() => {
|
||||||
|
const byDay = new Map<string, { date: string; edr: number; customer: number }>();
|
||||||
|
for (const ret of returnedControls.filteredRows) {
|
||||||
|
const day = toDayString(ret.returnDate);
|
||||||
|
if (!day) continue;
|
||||||
|
const entry = byDay.get(day) ?? { date: day, edr: 0, customer: 0 };
|
||||||
|
if (ret.returnedBy === "CUSTOMER") entry.customer += 1;
|
||||||
|
else entry.edr += 1;
|
||||||
|
byDay.set(day, entry);
|
||||||
|
}
|
||||||
|
return [...byDay.values()].sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
}, [returnedControls.filteredRows]);
|
||||||
|
|
||||||
|
const returnsByStatus = useMemo(
|
||||||
|
() =>
|
||||||
|
RETURN_STATUS_ORDER.map((status) => ({
|
||||||
|
label: RETURN_STATUS_LABEL[status],
|
||||||
|
value: returnedControls.filteredRows.filter((ret) => ret.status === status).length,
|
||||||
|
})),
|
||||||
|
[returnedControls.filteredRows],
|
||||||
|
);
|
||||||
|
|
||||||
const allGroups = useMemo(() => containerReturnsQuery.data ?? [], [containerReturnsQuery.data]);
|
const allGroups = useMemo(() => containerReturnsQuery.data ?? [], [containerReturnsQuery.data]);
|
||||||
const filteredGroups = useMemo(() => {
|
const filteredGroups = useMemo(() => {
|
||||||
@@ -271,6 +324,103 @@ export default function ContainerReturnsPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const returnedColumns: ColumnDef<EmptyContainerReturn>[] = [
|
||||||
|
{
|
||||||
|
id: "containerNumber",
|
||||||
|
header: "Container Number",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text fw={600} size="sm">
|
||||||
|
{row.original.containerNumber}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bookingRef",
|
||||||
|
header: "Booking Ref",
|
||||||
|
cell: ({ row }) => (row.original.bookingId ? "Associated" : "—"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "returnedBy",
|
||||||
|
header: "Returned By",
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.original.returnedBy ? (
|
||||||
|
<Badge size="sm" color={row.original.returnedBy === "EDR" ? "edr-green" : "orange"}>
|
||||||
|
{row.original.returnedBy === "EDR" ? "EDR Last Mile" : "Customer Self-Haul"}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
"—"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "returnDate",
|
||||||
|
header: "Returned Date",
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.original.returnDate ? new Date(row.original.returnDate).toLocaleDateString() : "—",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "facility",
|
||||||
|
header: "Facility",
|
||||||
|
cell: ({ row }) => row.original.facility || "—",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "yard",
|
||||||
|
header: "Yard",
|
||||||
|
cell: ({ row }) => row.original.yard || "—",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "condition",
|
||||||
|
header: "Condition",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Text size="sm" lineClamp={2}>
|
||||||
|
{row.original.condition || "—"}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "status",
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Badge size="sm">
|
||||||
|
{RETURN_STATUS_LABEL[row.original.status] ?? row.original.status}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "action",
|
||||||
|
header: "Action",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const ret = row.original;
|
||||||
|
const nextStatus = RETURN_STATUS_ORDER[RETURN_STATUS_ORDER.indexOf(ret.status) + 1];
|
||||||
|
return (
|
||||||
|
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
onClick={() => setHistoryRow(ret)}
|
||||||
|
title="View status history"
|
||||||
|
>
|
||||||
|
<History size={14} />
|
||||||
|
</ActionIcon>
|
||||||
|
{nextStatus ? (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="light"
|
||||||
|
loading={advanceStatusMutation.isPending && advanceStatusMutation.variables === ret.id}
|
||||||
|
onClick={() => advanceStatusMutation.mutate(ret.id)}
|
||||||
|
>
|
||||||
|
Advance to {RETURN_STATUS_LABEL[nextStatus]}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Text size="xs" c="dimmed">
|
||||||
|
Done
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const activeGroup = activeKey ? (filteredGroups.find((g) => `${g.returnType.toLowerCase()}-${g.bookingId}` === activeKey) ?? null) : null;
|
const activeGroup = activeKey ? (filteredGroups.find((g) => `${g.returnType.toLowerCase()}-${g.bookingId}` === activeKey) ?? null) : null;
|
||||||
|
|
||||||
if (queueLoading || containerReturnsQuery.isLoading) {
|
if (queueLoading || containerReturnsQuery.isLoading) {
|
||||||
@@ -305,73 +455,45 @@ export default function ContainerReturnsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{filteredReturnedContainers.length > 0 && (
|
{returnedContainers.length > 0 && (
|
||||||
<>
|
<Card withBorder radius="lg" p="md" mb="lg">
|
||||||
<Text fw={600} mb="xs">Returned Containers</Text>
|
<Stack gap="md">
|
||||||
<Table.ScrollContainer minWidth={1000} mb="lg">
|
<Text fw={600}>Returned Containers</Text>
|
||||||
<Table striped highlightOnHover verticalSpacing="xs">
|
<ListControls
|
||||||
<Table.Thead>
|
search={returnedControls.search}
|
||||||
<Table.Tr>
|
onSearchChange={returnedControls.setSearch}
|
||||||
<Table.Th>Container Number</Table.Th>
|
searchPlaceholder="Search container, facility, condition…"
|
||||||
<Table.Th>Booking Ref</Table.Th>
|
dateFrom={returnedControls.dateFrom}
|
||||||
<Table.Th>Returned By</Table.Th>
|
onDateFromChange={returnedControls.setDateFrom}
|
||||||
<Table.Th>Returned Date</Table.Th>
|
dateTo={returnedControls.dateTo}
|
||||||
<Table.Th>Facility</Table.Th>
|
onDateToChange={returnedControls.setDateTo}
|
||||||
<Table.Th>Yard</Table.Th>
|
dateLabel="Returned"
|
||||||
<Table.Th>Condition</Table.Th>
|
hasFilters={returnedControls.hasFilters || Boolean(statusFilter)}
|
||||||
<Table.Th>Status</Table.Th>
|
onReset={() => {
|
||||||
<Table.Th ta="right">Action</Table.Th>
|
returnedControls.reset();
|
||||||
</Table.Tr>
|
setStatusFilter(null);
|
||||||
</Table.Thead>
|
}}
|
||||||
<Table.Tbody>
|
>
|
||||||
{filteredReturnedContainers.map((ret: any) => {
|
<Select
|
||||||
const nextStatus = RETURN_STATUS_ORDER[RETURN_STATUS_ORDER.indexOf(ret.status) + 1];
|
placeholder="Status"
|
||||||
return (
|
value={statusFilter}
|
||||||
<Table.Tr key={ret.id}>
|
onChange={setStatusFilter}
|
||||||
<Table.Td>{ret.containerNumber}</Table.Td>
|
data={RETURN_STATUS_ORDER.map((status) => ({
|
||||||
<Table.Td>{ret.bookingId ? "Associated" : "—"}</Table.Td>
|
value: status,
|
||||||
<Table.Td>
|
label: RETURN_STATUS_LABEL[status],
|
||||||
{ret.returnedBy ? (
|
}))}
|
||||||
<Badge size="sm" color={ret.returnedBy === "EDR" ? "edr-green" : "blue"}>
|
clearable
|
||||||
{ret.returnedBy === "EDR" ? "EDR Last Mile" : "Customer Self-Haul"}
|
w={200}
|
||||||
</Badge>
|
/>
|
||||||
) : (
|
</ListControls>
|
||||||
"—"
|
<DataTable
|
||||||
)}
|
columns={returnedColumns}
|
||||||
</Table.Td>
|
data={returnedControls.pagedRows}
|
||||||
<Table.Td>{ret.returnDate ? new Date(ret.returnDate).toLocaleDateString() : "—"}</Table.Td>
|
containerClassName="border-0 shadow-none"
|
||||||
<Table.Td>{ret.facility || "—"}</Table.Td>
|
{...returnedControls.tableProps}
|
||||||
<Table.Td>{ret.yard || "—"}</Table.Td>
|
/>
|
||||||
<Table.Td>{ret.condition || "—"}</Table.Td>
|
</Stack>
|
||||||
<Table.Td>
|
</Card>
|
||||||
<Badge size="sm">{RETURN_STATUS_LABEL[ret.status as EmptyContainerReturnStatus] ?? ret.status}</Badge>
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td ta="right">
|
|
||||||
<Group gap="xs" justify="flex-end" wrap="nowrap">
|
|
||||||
<ActionIcon variant="subtle" color="gray" onClick={() => setHistoryRow(ret)} title="View status history">
|
|
||||||
<History size={14} />
|
|
||||||
</ActionIcon>
|
|
||||||
{nextStatus ? (
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
variant="light"
|
|
||||||
loading={advanceStatusMutation.isPending && advanceStatusMutation.variables === ret.id}
|
|
||||||
onClick={() => advanceStatusMutation.mutate(ret.id)}
|
|
||||||
>
|
|
||||||
Advance to {RETURN_STATUS_LABEL[nextStatus]}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Text size="xs" c="dimmed">Done</Text>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
</Table.ScrollContainer>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{filteredGroups.length === 0 ? (
|
{filteredGroups.length === 0 ? (
|
||||||
@@ -471,6 +593,26 @@ export default function ContainerReturnsPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{returnedContainers.length > 0 && (
|
||||||
|
<SimpleGrid cols={{ base: 1, lg: 2 }} spacing="md" mt="lg">
|
||||||
|
<OverviewStackedBarChart
|
||||||
|
title="Returns per day by truck type"
|
||||||
|
data={returnsPerDay}
|
||||||
|
series={RETURNED_BY_SERIES}
|
||||||
|
formatXLabel={(value) =>
|
||||||
|
new Date(value).toLocaleDateString(undefined, { day: "numeric", month: "short" })
|
||||||
|
}
|
||||||
|
emptyMessage="No returns in this range"
|
||||||
|
/>
|
||||||
|
<OverviewHorizontalBarChart
|
||||||
|
title="Returns by status"
|
||||||
|
data={returnsByStatus}
|
||||||
|
valueLabel="Containers"
|
||||||
|
emptyMessage="No returns in this range"
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
)}
|
||||||
|
|
||||||
<ContainerReturnModal
|
<ContainerReturnModal
|
||||||
opened={returnModalOpen}
|
opened={returnModalOpen}
|
||||||
onClose={() => setReturnModalOpen(false)}
|
onClose={() => setReturnModalOpen(false)}
|
||||||
|
|||||||
Reference in New Issue
Block a user