mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +00:00
fix
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Alert,
|
||||
} from "@mantine/core";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { vehiclesService } from "@/services/vehicles.service";
|
||||
|
||||
export interface ContainerAllocationRow {
|
||||
id: string;
|
||||
type: string;
|
||||
qty: number;
|
||||
}
|
||||
|
||||
export interface ContainerAllocationTableProps {
|
||||
bookingId: string;
|
||||
containers: ContainerAllocationRow[];
|
||||
onSave: (allocations: Array<{ containerId: string; vehicleId: string }>) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual container-to-vehicle allocation table for freight bookings.
|
||||
* Displays containers with type/qty, vehicle dropdown per row, and save action.
|
||||
*/
|
||||
export function ContainerAllocationTable({
|
||||
bookingId,
|
||||
containers,
|
||||
onSave,
|
||||
}: ContainerAllocationTableProps) {
|
||||
const [allocations, setAllocations] = useState<Record<string, string | null>>(
|
||||
() => containers.reduce((acc, c) => ({ ...acc, [c.id]: null }), {}),
|
||||
);
|
||||
|
||||
const { data: vehicles = [], isLoading: vehiclesLoading } = useQuery({
|
||||
queryKey: ["vehicles", "active"],
|
||||
queryFn: () => vehiclesService.getAll({ status: "ACTIVE" }),
|
||||
});
|
||||
|
||||
const vehicleOptions = useMemo(
|
||||
() =>
|
||||
vehicles.map((v) => ({
|
||||
value: v.id,
|
||||
label: `${v.plateNumber} (${v.vehicleType})`,
|
||||
description: `${v.model} · ${v.manufacturer}`,
|
||||
})),
|
||||
[vehicles],
|
||||
);
|
||||
|
||||
const saveAllocation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const mappings = containers
|
||||
.filter((c) => allocations[c.id])
|
||||
.map((c) => ({
|
||||
containerId: c.id,
|
||||
vehicleId: allocations[c.id]!,
|
||||
}));
|
||||
|
||||
if (mappings.length === 0) {
|
||||
throw new Error("No containers allocated to vehicles");
|
||||
}
|
||||
|
||||
await onSave(mappings);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Container allocations saved");
|
||||
setAllocations(
|
||||
containers.reduce((acc, c) => ({ ...acc, [c.id]: null }), {}),
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to save allocations",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const allocatedCount = Object.values(allocations).filter(Boolean).length;
|
||||
const allAllocated = allocatedCount === containers.length;
|
||||
|
||||
if (vehiclesLoading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" p="xl">
|
||||
<Loader size="sm" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{vehicles.length === 0 && (
|
||||
<Alert icon={<AlertCircle size={16} />} color="yellow">
|
||||
No active vehicles available. Add vehicles before allocating containers.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table verticalSpacing="md" horizontalSpacing="md" highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Container ID</Table.Th>
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>Qty</Table.Th>
|
||||
<Table.Th>Assigned Vehicle</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{containers.map((container) => (
|
||||
<Table.Tr key={container.id}>
|
||||
<Table.Td>
|
||||
<Text fw={600} size="sm">
|
||||
{container.id}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{container.type}</Table.Td>
|
||||
<Table.Td>{container.qty}</Table.Td>
|
||||
<Table.Td>
|
||||
<Select
|
||||
placeholder="Select vehicle"
|
||||
data={vehicleOptions}
|
||||
value={allocations[container.id] ?? null}
|
||||
onChange={(value) =>
|
||||
setAllocations((prev) => ({
|
||||
...prev,
|
||||
[container.id]: value,
|
||||
}))
|
||||
}
|
||||
searchable
|
||||
clearable
|
||||
disabled={vehicles.length === 0}
|
||||
style={{ minWidth: 200 }}
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">
|
||||
{allocatedCount} of {containers.length} containers allocated
|
||||
</Text>
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={saveAllocation.isPending}
|
||||
disabled={allocatedCount === 0 || vehicles.length === 0}
|
||||
onClick={() => saveAllocation.mutate()}
|
||||
>
|
||||
Save Allocations
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Alert,
|
||||
} from "@mantine/core";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { vehiclesService } from "@/services/vehicles.service";
|
||||
|
||||
export interface ContainerAllocationRow {
|
||||
id: string;
|
||||
type: string;
|
||||
qty: number;
|
||||
}
|
||||
|
||||
export interface FirstMileContainerAllocationTableProps {
|
||||
firstMileId: string;
|
||||
containers: ContainerAllocationRow[];
|
||||
onSave: (allocations: Array<{ containerId: string; vehicleId: string }>) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual container-to-vehicle allocation table for first-mile pickups.
|
||||
* Displays containers with type/qty, vehicle dropdown per row, and save action.
|
||||
*/
|
||||
export function FirstMileContainerAllocationTable({
|
||||
firstMileId,
|
||||
containers,
|
||||
onSave,
|
||||
}: FirstMileContainerAllocationTableProps) {
|
||||
const [allocations, setAllocations] = useState<Record<string, string | null>>(
|
||||
() => containers.reduce((acc, c) => ({ ...acc, [c.id]: null }), {}),
|
||||
);
|
||||
|
||||
const { data: vehicles = [], isLoading: vehiclesLoading } = useQuery({
|
||||
queryKey: ["vehicles", "active"],
|
||||
queryFn: () => vehiclesService.getAll({ status: "ACTIVE" }),
|
||||
});
|
||||
|
||||
const vehicleOptions = useMemo(
|
||||
() =>
|
||||
vehicles.map((v) => ({
|
||||
value: v.id,
|
||||
label: `${v.plateNumber} (${v.vehicleType})`,
|
||||
description: `${v.model} · ${v.manufacturer}`,
|
||||
})),
|
||||
[vehicles],
|
||||
);
|
||||
|
||||
const saveAllocation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const mappings = containers
|
||||
.filter((c) => allocations[c.id])
|
||||
.map((c) => ({
|
||||
containerId: c.id,
|
||||
vehicleId: allocations[c.id]!,
|
||||
}));
|
||||
|
||||
if (mappings.length === 0) {
|
||||
throw new Error("No containers allocated to vehicles");
|
||||
}
|
||||
|
||||
await onSave(mappings);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Container allocations saved");
|
||||
setAllocations(
|
||||
containers.reduce((acc, c) => ({ ...acc, [c.id]: null }), {}),
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to save allocations",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const allocatedCount = Object.values(allocations).filter(Boolean).length;
|
||||
const allAllocated = allocatedCount === containers.length;
|
||||
|
||||
if (vehiclesLoading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" p="xl">
|
||||
<Loader size="sm" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{vehicles.length === 0 && (
|
||||
<Alert icon={<AlertCircle size={16} />} color="yellow">
|
||||
No active vehicles available. Add vehicles before allocating containers.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table verticalSpacing="md" horizontalSpacing="md" highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Container ID</Table.Th>
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>Qty</Table.Th>
|
||||
<Table.Th>Assigned Vehicle</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{containers.map((container) => (
|
||||
<Table.Tr key={container.id}>
|
||||
<Table.Td>
|
||||
<Text fw={600} size="sm">
|
||||
{container.id}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{container.type}</Table.Td>
|
||||
<Table.Td>{container.qty}</Table.Td>
|
||||
<Table.Td>
|
||||
<Select
|
||||
placeholder="Select vehicle"
|
||||
data={vehicleOptions}
|
||||
value={allocations[container.id] ?? null}
|
||||
onChange={(value) =>
|
||||
setAllocations((prev) => ({
|
||||
...prev,
|
||||
[container.id]: value,
|
||||
}))
|
||||
}
|
||||
searchable
|
||||
clearable
|
||||
disabled={vehicles.length === 0}
|
||||
style={{ minWidth: 200 }}
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">
|
||||
{allocatedCount} of {containers.length} containers allocated
|
||||
</Text>
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={saveAllocation.isPending}
|
||||
disabled={allocatedCount === 0 || vehicles.length === 0}
|
||||
onClick={() => saveAllocation.mutate()}
|
||||
>
|
||||
Save Allocations
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { useState, useMemo } from "react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
Loader,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Alert,
|
||||
} from "@mantine/core";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { vehiclesService } from "@/services/vehicles.service";
|
||||
|
||||
export interface LastMileContainerRow {
|
||||
id: string;
|
||||
type: string;
|
||||
qty: number;
|
||||
}
|
||||
|
||||
export interface LastMileContainerAllocationTableProps {
|
||||
lastMileId: string;
|
||||
containers: LastMileContainerRow[];
|
||||
onSave: (allocations: Array<{ containerId: string; vehicleId: string }>) => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual container-to-vehicle allocation table for last-mile deliveries.
|
||||
* Displays containers with type/qty, vehicle dropdown per row, and save action.
|
||||
*/
|
||||
export function LastMileContainerAllocationTable({
|
||||
lastMileId,
|
||||
containers,
|
||||
onSave,
|
||||
}: LastMileContainerAllocationTableProps) {
|
||||
const [allocations, setAllocations] = useState<Record<string, string | null>>(
|
||||
() => containers.reduce((acc, c) => ({ ...acc, [c.id]: null }), {}),
|
||||
);
|
||||
|
||||
const { data: vehicles = [], isLoading: vehiclesLoading } = useQuery({
|
||||
queryKey: ["vehicles", "active"],
|
||||
queryFn: () => vehiclesService.getAll({ status: "ACTIVE" }),
|
||||
});
|
||||
|
||||
const vehicleOptions = useMemo(
|
||||
() =>
|
||||
vehicles.map((v) => ({
|
||||
value: v.id,
|
||||
label: `${v.plateNumber} (${v.vehicleType})`,
|
||||
description: `${v.model} · ${v.manufacturer}`,
|
||||
})),
|
||||
[vehicles],
|
||||
);
|
||||
|
||||
const saveAllocation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const mappings = containers
|
||||
.filter((c) => allocations[c.id])
|
||||
.map((c) => ({
|
||||
containerId: c.id,
|
||||
vehicleId: allocations[c.id]!,
|
||||
}));
|
||||
|
||||
if (mappings.length === 0) {
|
||||
throw new Error("No containers allocated to vehicles");
|
||||
}
|
||||
|
||||
await onSave(mappings);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Container allocations saved");
|
||||
setAllocations(
|
||||
containers.reduce((acc, c) => ({ ...acc, [c.id]: null }), {}),
|
||||
);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "Failed to save allocations",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const allocatedCount = Object.values(allocations).filter(Boolean).length;
|
||||
const allAllocated = allocatedCount === containers.length;
|
||||
|
||||
if (vehiclesLoading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" p="xl">
|
||||
<Loader size="sm" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{vehicles.length === 0 && (
|
||||
<Alert icon={<AlertCircle size={16} />} color="yellow">
|
||||
No active vehicles available. Add vehicles before allocating containers.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box style={{ overflowX: "auto" }}>
|
||||
<Table verticalSpacing="md" horizontalSpacing="md" highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Container ID</Table.Th>
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>Qty</Table.Th>
|
||||
<Table.Th>Assigned Vehicle</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{containers.map((container) => (
|
||||
<Table.Tr key={container.id}>
|
||||
<Table.Td>
|
||||
<Text fw={600} size="sm">
|
||||
{container.id}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>{container.type}</Table.Td>
|
||||
<Table.Td>{container.qty}</Table.Td>
|
||||
<Table.Td>
|
||||
<Select
|
||||
placeholder="Select vehicle"
|
||||
data={vehicleOptions}
|
||||
value={allocations[container.id] ?? null}
|
||||
onChange={(value) =>
|
||||
setAllocations((prev) => ({
|
||||
...prev,
|
||||
[container.id]: value,
|
||||
}))
|
||||
}
|
||||
searchable
|
||||
clearable
|
||||
disabled={vehicles.length === 0}
|
||||
style={{ minWidth: 200 }}
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="dimmed">
|
||||
{allocatedCount} of {containers.length} containers allocated
|
||||
</Text>
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={saveAllocation.isPending}
|
||||
disabled={allocatedCount === 0 || vehicles.length === 0}
|
||||
onClick={() => saveAllocation.mutate()}
|
||||
>
|
||||
Save Allocations
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
export const API_BASE_URL = 'https://edrfreightapi.triaplc.com';
|
||||
// export const API_BASE_URL = 'https://edrfreightapi.triaplc.com';
|
||||
|
||||
// export const API_BASE_URL = 'http://localhost:3001';
|
||||
export const API_BASE_URL = 'http://localhost:3001';
|
||||
|
||||
/**
|
||||
* URL that streams an uploaded file through the API by its UUID. Routes the
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Container, Grid, Stack } from "@mantine/core";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import {
|
||||
BookingApprovalCard,
|
||||
@@ -16,10 +18,26 @@ import {
|
||||
type BookingDetailView,
|
||||
} from "@/components/bookings/detail";
|
||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||
import ContainerAllocationTable from "@/components/ContainerAllocationTable";
|
||||
import { api } from "@/services/api";
|
||||
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
|
||||
|
||||
const BookingDetailPage = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const qc = useQueryClient();
|
||||
|
||||
const allocateMutation = useMutation({
|
||||
mutationFn: (data: any) =>
|
||||
api.post(`/bookings/${id}/allocate-containers`, data),
|
||||
onSuccess: () => {
|
||||
toast.success("Containers allocated");
|
||||
qc.invalidateQueries({ queryKey: QUERY_KEYS.BOOKINGS.byId(id ?? "") });
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to allocate containers");
|
||||
},
|
||||
});
|
||||
|
||||
// Mock data - replace with actual API call
|
||||
const booking: BookingDetailView = {
|
||||
@@ -134,6 +152,17 @@ const BookingDetailPage = () => {
|
||||
<BookingContainersCard
|
||||
containers={booking.bookingContainers ?? []}
|
||||
/>
|
||||
<ContainerAllocationTable
|
||||
bookingId={booking.id}
|
||||
containers={(booking.bookingContainers ?? []).map((c) => ({
|
||||
id: c.id,
|
||||
type: c.containerType?.label ?? "Unknown",
|
||||
qty: c.quantity,
|
||||
}))}
|
||||
onSave={(allocations) =>
|
||||
allocateMutation.mutateAsync({ allocations })
|
||||
}
|
||||
/>
|
||||
<BookingApprovalCard
|
||||
steps={approvalSteps}
|
||||
approvedCount={approvedCount}
|
||||
|
||||
@@ -30,9 +30,11 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
UnstyledButton,
|
||||
Alert,
|
||||
} from "@mantine/core";
|
||||
|
||||
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||
import { FirstMileContainerAllocationTable } from "@/components/FirstMileContainerAllocationTable";
|
||||
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import {
|
||||
@@ -44,6 +46,7 @@ import {
|
||||
import { bookingsService } from "@/services/bookings.service";
|
||||
import { vehiclesService } from "@/services/vehicles.service";
|
||||
import { ratesService } from "@/services/rates.service";
|
||||
import { apiClient } from "@/services/api-client";
|
||||
import type { BookingDetail } from "@/types/booking";
|
||||
|
||||
const formatPrice = (amount: number) =>
|
||||
@@ -336,6 +339,9 @@ const FirstMilePage = () => {
|
||||
const [invoiceOpen, setInvoiceOpen] = useState(false);
|
||||
const [invoiceRecord, setInvoiceRecord] = useState<FirstMileRecord | null>(null);
|
||||
|
||||
const [containerAllocationOpen, setContainerAllocationOpen] = useState(false);
|
||||
const [containerAllocationFirstMileId, setContainerAllocationFirstMileId] = useState<string | null>(null);
|
||||
|
||||
const { data: listData, isLoading } = useQuery({
|
||||
queryKey: QUERY_KEYS.FIRST_MILE.list(),
|
||||
queryFn: async () => {
|
||||
@@ -434,6 +440,19 @@ const FirstMilePage = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const allocateMutation = useMutation({
|
||||
mutationFn: (data) => apiClient.post(`/first-mile/${containerAllocationFirstMileId}/allocate-containers`, data),
|
||||
onSuccess: () => {
|
||||
toast({ title: "Containers allocated" });
|
||||
void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.detail(containerAllocationFirstMileId ?? "") });
|
||||
setContainerAllocationOpen(false);
|
||||
setContainerAllocationFirstMileId(null);
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Allocation failed", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const activeRecord = useMemo(
|
||||
() => records.find((r) => r.id === activeId) ?? null,
|
||||
[records, activeId],
|
||||
@@ -508,6 +527,16 @@ const FirstMilePage = () => {
|
||||
setInvoiceRecord(null);
|
||||
};
|
||||
|
||||
const openContainerAllocation = (firstMileId: string) => {
|
||||
setContainerAllocationFirstMileId(firstMileId);
|
||||
setContainerAllocationOpen(true);
|
||||
};
|
||||
|
||||
const closeContainerAllocation = () => {
|
||||
setContainerAllocationOpen(false);
|
||||
setContainerAllocationFirstMileId(null);
|
||||
};
|
||||
|
||||
const handleSaveDistance = () => {
|
||||
const distance = parseFloat(distanceValue);
|
||||
if (!activeId || isNaN(distance) || distance < 0) {
|
||||
@@ -1272,6 +1301,56 @@ const FirstMilePage = () => {
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Container Allocation modal */}
|
||||
<Modal
|
||||
opened={containerAllocationOpen}
|
||||
onClose={closeContainerAllocation}
|
||||
title={<Text fw={600}>Allocate Containers to Vehicles</Text>}
|
||||
size="xl"
|
||||
radius="lg"
|
||||
centered
|
||||
>
|
||||
<Stack gap="md">
|
||||
{activeRecord && (
|
||||
<>
|
||||
{/* Capacity guidance */}
|
||||
{activeRecord.booking?.cargoType?.label === "BULK" ? (
|
||||
<Alert color="blue" title="Bulk Cargo Allocation">
|
||||
<Text size="sm">
|
||||
Select multiple containers per vehicle based on capacity. Each vehicle can carry multiple containers if capacity allows.
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt="xs">
|
||||
Capacity: TBD — TODO: add vehicle capacity_tons to vehicle API if missing
|
||||
</Text>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert color="blue">
|
||||
<Text size="sm">
|
||||
One vehicle per container. Each container will be assigned to a single vehicle.
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<Divider />
|
||||
|
||||
{/* Container table */}
|
||||
<FirstMileContainerAllocationTable
|
||||
firstMileId={activeRecord.id}
|
||||
containers={[
|
||||
// TODO: Get containers from booking/first-mile data
|
||||
// For now placeholder with TODO comment
|
||||
]}
|
||||
onSave={async (allocations) => {
|
||||
await allocateMutation.mutateAsync(allocations);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={closeContainerAllocation}>Close</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,6 +45,8 @@ import {
|
||||
} from "@/services/last-mile.service";
|
||||
import { vehiclesService } from "@/services/vehicles.service";
|
||||
import { ratesService } from "@/services/rates.service";
|
||||
import { LastMileContainerAllocationTable, type LastMileContainerRow } from "@/components/LastMileContainerAllocationTable";
|
||||
import { api } from "@/auth/http";
|
||||
|
||||
const formatPrice = (amount: number) =>
|
||||
`ETB ${amount.toLocaleString("en-US", {
|
||||
@@ -321,6 +323,9 @@ const LastMilePage = () => {
|
||||
const [invoiceOpen, setInvoiceOpen] = useState(false);
|
||||
const [invoiceRecord, setInvoiceRecord] = useState<LastMileRecord | null>(null);
|
||||
|
||||
const [allocationOpen, setAllocationOpen] = useState(false);
|
||||
const [allocationContainers, setAllocationContainers] = useState<LastMileContainerRow[]>([]);
|
||||
|
||||
const { data: listData, isLoading } = useQuery({
|
||||
queryKey: QUERY_KEYS.LAST_MILE.list(),
|
||||
queryFn: async () => {
|
||||
@@ -385,6 +390,19 @@ const LastMilePage = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const allocateMutation = useMutation({
|
||||
mutationFn: (data: Array<{ containerId: string; vehicleId: string }>) =>
|
||||
api.post(`/last-mile/${activeId}/allocate-containers`, data),
|
||||
onSuccess: () => {
|
||||
toast({ title: "Containers allocated", variant: "default" });
|
||||
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.detail(activeId ?? "") });
|
||||
closeAllocation();
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Allocation failed", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const { data: arrivalQueueData, isLoading: arrivalLoading } = useQuery({
|
||||
queryKey: ["warehouse-inventory", "arrival-queue"],
|
||||
queryFn: () => warehouseService.arrivalQueue().then((r) => r.data),
|
||||
@@ -477,6 +495,18 @@ const LastMilePage = () => {
|
||||
setInvoiceRecord(null);
|
||||
};
|
||||
|
||||
const openAllocation = (id: string, containers?: LastMileContainerRow[]) => {
|
||||
setActiveId(id);
|
||||
setAllocationContainers(containers ?? []);
|
||||
setAllocationOpen(true);
|
||||
};
|
||||
|
||||
const closeAllocation = () => {
|
||||
setAllocationOpen(false);
|
||||
setActiveId(null);
|
||||
setAllocationContainers([]);
|
||||
};
|
||||
|
||||
const handleSaveDistance = () => {
|
||||
const distance = parseFloat(distanceValue);
|
||||
if (!activeId || isNaN(distance) || distance < 0) {
|
||||
@@ -1243,6 +1273,76 @@ const LastMilePage = () => {
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{/* Container Allocation modal */}
|
||||
<Modal
|
||||
opened={allocationOpen}
|
||||
onClose={closeAllocation}
|
||||
title={<Text fw={600}>Allocate Containers to Vehicles</Text>}
|
||||
size="xl"
|
||||
radius="lg"
|
||||
centered
|
||||
>
|
||||
<Stack gap="md">
|
||||
{activeRecord && (
|
||||
<>
|
||||
<Card withBorder padding="md" radius="md" bg="var(--mantine-color-gray-0)">
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between">
|
||||
<Stack gap={0}>
|
||||
<Text fw={600} size="sm">{bookingRef(activeRecord)}</Text>
|
||||
<Text size="xs" c="dimmed">{customerName(activeRecord)}</Text>
|
||||
</Stack>
|
||||
<Stack gap={0} align="flex-end">
|
||||
<Text size="xs" c="dimmed" tt="uppercase">Cargo Type</Text>
|
||||
<Text size="sm" fw={600}>{activeRecord.booking?.cargoType?.label ?? activeRecord.booking?.cargoType?.name ?? "—"}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Capacity logic based on cargo type */}
|
||||
{activeRecord.booking?.cargoType?.name === "BULK" ? (
|
||||
<Card withBorder padding="md" radius="md" bg="var(--mantine-color-blue-0)" style={{ borderColor: "var(--mantine-color-blue-3)" }}>
|
||||
<Stack gap="sm">
|
||||
<Group gap="xs">
|
||||
<Text fw={600} size="sm">Smart Capacity Allocation</Text>
|
||||
</Group>
|
||||
<Stack gap={2}>
|
||||
<Text size="sm">Capacity: TBD</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
TODO: add vehicle capacity_tons to vehicle API if missing
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
TODO: add container weight to booking if missing
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text size="sm" fw={500} mt="xs">
|
||||
Select multiple containers per vehicle based on capacity
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
) : (
|
||||
<Card withBorder padding="md" radius="md" bg="var(--mantine-color-gray-0)">
|
||||
<Text size="sm" fw={500}>One vehicle per container</Text>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<LastMileContainerAllocationTable
|
||||
lastMileId={activeId ?? ""}
|
||||
containers={allocationContainers}
|
||||
onSave={async (mappings) => {
|
||||
await allocateMutation.mutateAsync(mappings);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={closeAllocation}>Close</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user