Merge branch 'dev' into freight/feat/invoice

This commit is contained in:
Nathnael Wondisha
2026-06-29 17:24:29 +03:00
committed by GitHub
42 changed files with 2337 additions and 207 deletions

View File

@@ -314,6 +314,7 @@ const FirstMilePage = () => {
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState<StatusFilter>("ALL");
const [filterPostPaymentPending, setFilterPostPaymentPending] = useState(false);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const [assignOpen, setAssignOpen] = useState(false);
@@ -529,6 +530,7 @@ const FirstMilePage = () => {
};
const matchesFilter = (r: FirstMileRecord) => {
if (filterPostPaymentPending && r.isPostPaymentCompleted) return false;
switch (statusFilter) {
case "ALL": return true;
case "ASSIGNED": return isAssigned(r);
@@ -566,7 +568,7 @@ const FirstMilePage = () => {
.includes(term);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [records, search, statusFilter]);
}, [records, search, statusFilter, filterPostPaymentPending]);
const pageCount = Math.max(1, Math.ceil(filteredRecords.length / pagination.pageSize));
const pagedRecords = useMemo(() => {
@@ -887,6 +889,17 @@ const FirstMilePage = () => {
</Button>
);
})}
<Button
size="xs"
variant={filterPostPaymentPending ? "filled" : "default"}
styles={{ label: { fontWeight: 500 } }}
onClick={() => {
setFilterPostPaymentPending(!filterPostPaymentPending);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
>
Post Payment Pending
</Button>
</Group>
</Stack>
</Box>

View File

@@ -298,6 +298,7 @@ const LastMilePage = () => {
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState<StatusFilter>("ALL");
const [filterPostPaymentPending, setFilterPostPaymentPending] = useState(false);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const [assignOpen, setAssignOpen] = useState(false);
@@ -508,6 +509,7 @@ const LastMilePage = () => {
);
const matchesFilter = (r: LastMileRecord) => {
if (filterPostPaymentPending && r.isPostPaymentCompleted) return false;
switch (statusFilter) {
case "ALL": return true;
case "ASSIGNED": return isAssigned(r);
@@ -545,7 +547,7 @@ const LastMilePage = () => {
.includes(term);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [records, search, statusFilter]);
}, [records, search, statusFilter, filterPostPaymentPending]);
const pageCount = Math.max(1, Math.ceil(filteredRecords.length / pagination.pageSize));
const pagedRecords = useMemo(() => {
@@ -866,6 +868,17 @@ const LastMilePage = () => {
</Button>
);
})}
<Button
size="xs"
variant={filterPostPaymentPending ? "filled" : "default"}
styles={{ label: { fontWeight: 500 } }}
onClick={() => {
setFilterPostPaymentPending(!filterPostPaymentPending);
setPagination((p) => ({ ...p, pageIndex: 0 }));
}}
>
Post Payment Pending
</Button>
</Group>
</Stack>
</Box>

View File

@@ -296,9 +296,15 @@ export default function TrainScheduleV2DetailPage() {
const canDispatch = schedule.status === "SCHEDULED";
const finalizeStep = hasContainerStep ? 3 : 2;
const canModifyBookings = canEditBookings && !["DISPATCHED", "ARRIVED"].includes(schedule.status);
const canPrintMarshalling = schedule.direction === "IMPORT" || schedule.direction === "EXPORT";
const canPrintMarshalling =
(schedule.direction === "IMPORT" || schedule.direction === "EXPORT") &&
["DISPATCHED", "ARRIVED"].includes(schedule.status);
const openMarshallingDocument = async () => {
const openMarshallingDocument = async (options?: {
title?: string;
successDescription?: string;
errorTitle?: string;
}) => {
const pdfWindow = window.open("", "_blank");
try {
const blob = await downloadMarshalling.mutateAsync({
@@ -309,15 +315,17 @@ export default function TrainScheduleV2DetailPage() {
const filename = `${prefix}-${schedule.trainNumber ?? scheduleId}.pdf`;
const opened = openPdfBlob(blob, filename, pdfWindow);
toast({
title: "Marshalling document ready",
description: opened
? "The PDF opened in a browser tab for printing or saving."
: "The browser blocked the preview tab, so the PDF was downloaded.",
title: options?.title ?? "Marshalling document ready",
description:
options?.successDescription ??
(opened
? "The PDF opened in a browser tab for printing or saving."
: "The browser blocked the preview tab, so the PDF was downloaded."),
});
} catch (error) {
pdfWindow?.close();
toast({
title: "Could not open marshalling document",
title: options?.errorTitle ?? "Could not open marshalling document",
description: parseError(error, "Make sure the train has wagon allocations, then try again."),
variant: "destructive",
});
@@ -708,7 +716,11 @@ export default function TrainScheduleV2DetailPage() {
onClick={async () => {
try {
await dispatch.mutateAsync(scheduleId);
toast({ title: "Train dispatched" });
await openMarshallingDocument({
title: "Train dispatched",
successDescription: "Marshalling document generated for the dispatched train.",
errorTitle: "Train dispatched, but document could not open",
});
} catch (err) {
toast({
title: "Dispatch failed",

View File

@@ -231,8 +231,8 @@ export default function ArrivalQueuePage() {
<Table.Td ta="center">{train.totalCargoes}</Table.Td>
<Table.Td>
<Stack gap={2}>
<Badge variant="light" color={fullyUnloaded ? 'green' : 'teal'} size="sm">
{fullyUnloaded ? 'UNLOADED' : train.status}
<Badge variant="light" color="teal" size="sm">
{train.status}
</Badge>
<Text size="xs" c="dimmed">
{Math.max(unloadedBookings, 0)}/{train.totalBookings} unloaded

View File

@@ -66,14 +66,14 @@ const statusLabel = (status?: string | null) =>
: (status ?? 'PENDING').replace(/_/g, ' ');
function ExportTrainDetailRows({
scheduleId,
train,
onOpenHistory,
}: {
scheduleId: string;
train: ExportTrain;
onOpenHistory: (inventoryId: string) => void;
}) {
const navigate = useNavigate();
const { data: items = [], isLoading } = useExportDjiboutiTrainItems(scheduleId);
const { data: items = [], isLoading } = useExportDjiboutiTrainItems(train.scheduleId);
if (isLoading) {
return (
@@ -92,10 +92,11 @@ function ExportTrainDetailRows({
}
return (
<Table.ScrollContainer minWidth={1320}>
<Table.ScrollContainer minWidth={1420}>
<Table highlightOnHover verticalSpacing="xs">
<Table.Thead>
<Table.Tr>
<Table.Th>Wagon</Table.Th>
<Table.Th>Booking ID</Table.Th>
<Table.Th>Booking Reference</Table.Th>
<Table.Th>Customer ID</Table.Th>
@@ -115,6 +116,11 @@ function ExportTrainDetailRows({
<Table.Tbody>
{items.map((item: ExportTrainItem) => (
<Table.Tr key={`${item.bookingId}-${item.itemType}-${item.itemId ?? item.inventoryId ?? 'item'}`}>
<Table.Td>
<Text size="xs" fw={600}>
{item.sequenceNo ? `#${item.sequenceNo}` : '-'} {item.wagonNumber ?? ''}
</Text>
</Table.Td>
<Table.Td>{item.bookingId.slice(0, 8)}</Table.Td>
<Table.Td>
<Text size="sm" fw={600}>
@@ -384,7 +390,7 @@ export default function ExportDjiboutiUnloadingQueuePage() {
<Table.Tr>
<Table.Td colSpan={12} bg="var(--mantine-color-gray-0)">
<ExportTrainDetailRows
scheduleId={train.scheduleId}
train={train}
onOpenHistory={setHistoryInventoryId}
/>
</Table.Td>

View File

@@ -0,0 +1,28 @@
import { Button, Card } from '@mantine/core';
import { PackageSearch } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { PageContainer, PageHeader } from '@/components/page';
import { WarehouseFlowWorkbench } from '@/components/warehouses';
export default function ExportWarehouseFlowPage() {
const navigate = useNavigate();
return (
<PageContainer>
<PageHeader
title="Export Operations"
subtitle="Manage export receive, terminal inventory, loading readiness, loaded items, and dispatch flow."
action={
<Button variant="light" leftSection={<PackageSearch size={16} />} onClick={() => navigate('/dashboard/import-warehouse')}>
Import Operations
</Button>
}
/>
<Card>
<WarehouseFlowWorkbench direction="EXPORT" />
</Card>
</PageContainer>
);
}

View File

@@ -0,0 +1,28 @@
import { Button, Card } from '@mantine/core';
import { Truck } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { PageContainer, PageHeader } from '@/components/page';
import { WarehouseFlowWorkbench } from '@/components/warehouses';
export default function ImportWarehouseFlowPage() {
const navigate = useNavigate();
return (
<PageContainer>
<PageHeader
title="Import Operations"
subtitle="Manage import gate flow, marshalling handoff, arrived trains, unloaded bookings, and dispatch-ready inventory."
action={
<Button variant="light" leftSection={<Truck size={16} />} onClick={() => navigate('/dashboard/export-warehouse')}>
Export Operations
</Button>
}
/>
<Card>
<WarehouseFlowWorkbench direction="IMPORT" />
</Card>
</PageContainer>
);
}

View File

@@ -1,13 +1,12 @@
import { useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { Button, Card, Group, Select, Stack, TextInput } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { PackagePlus, Search } from 'lucide-react';
import { PackageOpen, Search, Truck } from 'lucide-react';
import { PageContainer, PageHeader } from '@/components/page';
import {
InventoryWorkbench,
ReceiveInventoryModal,
inventoryStatusOptions,
} from '@/components/warehouses';
import {
@@ -19,13 +18,13 @@ import {
import type { InventoryFilter, InventoryStatus } from '@/types/warehouse';
export default function WarehouseInventoryPage() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const initialStatus = (searchParams.get('status') as InventoryStatus | null) ?? undefined;
const [filter, setFilter] = useState<InventoryFilter>(
initialStatus ? { status: initialStatus } : {},
);
const [search, setSearch] = useState('');
const [modalOpen, setModalOpen] = useState(false);
const [debouncedSearch] = useDebouncedValue(search, 300);
const queryFilter = useMemo<InventoryFilter>(
@@ -57,9 +56,18 @@ export default function WarehouseInventoryPage() {
title="Warehouse Inventory"
subtitle="Track received items through the storage, reservation, loading and dispatch lifecycle."
action={
<Button leftSection={<PackagePlus size={16} />} onClick={() => setModalOpen(true)}>
Receive Inventory
</Button>
<Group gap="xs">
<Button
variant="light"
leftSection={<PackageOpen size={16} />}
onClick={() => navigate('/dashboard/import-warehouse')}
>
Import
</Button>
<Button leftSection={<Truck size={16} />} onClick={() => navigate('/dashboard/export-warehouse')}>
Export
</Button>
</Group>
}
/>
@@ -126,8 +134,6 @@ export default function WarehouseInventoryPage() {
<InventoryWorkbench items={inventoryQuery.data ?? []} isLoading={inventoryQuery.isLoading} />
</Stack>
</Card>
<ReceiveInventoryModal opened={modalOpen} onClose={() => setModalOpen(false)} />
</PageContainer>
);
}