fix first mile

This commit is contained in:
natib21
2026-06-23 15:40:21 +00:00
parent dbb9870cb6
commit 2737866366

View File

@@ -37,7 +37,9 @@ import {
type FirstMileRecord,
firstMileService,
} from "@/services/first-mile.service";
import { bookingsService } from "@/services/bookings.service";
import { vehiclesService } from "@/services/vehicles.service";
import type { BookingDetail } from "@/types/booking";
const formatPrice = (amount: number) =>
`ETB ${amount.toLocaleString("en-US", {
@@ -308,6 +310,12 @@ const FirstMilePage = () => {
const [activeId, setActiveId] = useState<string | null>(null);
const [vehicleValue, setVehicleValue] = useState<string | null>(null);
const [acceptOpen, setAcceptOpen] = useState(false);
const [acceptStep, setAcceptStep] = useState<1 | 2>(1);
const [selectedBooking, setSelectedBooking] = useState<BookingDetail | null>(null);
const [acceptVehicleValue, setAcceptVehicleValue] = useState<string | null>(null);
const [bookingSearch, setBookingSearch] = useState("");
const { data: listData, isLoading } = useQuery({
queryKey: QUERY_KEYS.FIRST_MILE.list(),
queryFn: async () => {
@@ -324,6 +332,13 @@ const FirstMilePage = () => {
},
});
const { data: paidBookingsData, isLoading: bookingsLoading } = useQuery({
queryKey: QUERY_KEYS.BOOKINGS.list({ status: "PAID" }),
queryFn: () => bookingsService.list({ status: "PAID", pageSize: 100 }),
enabled: acceptOpen,
});
const paidBookings = paidBookingsData?.items ?? [];
const records = listData?.data ?? [];
const vehicleOptions = useMemo(
@@ -346,6 +361,23 @@ const FirstMilePage = () => {
},
});
const acceptMutation = useMutation({
mutationFn: async ({ reference, vehicleId }: { reference: string; vehicleId: string | null }) => {
const res = await firstMileService.accept(reference);
const created = res.data;
if (vehicleId) await firstMileService.update(created.id, { vehicleId });
return created;
},
onSuccess: () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.ROOT });
toast({ title: "Booking accepted", description: "First-mile leg created successfully." });
closeAccept();
},
onError: () => {
toast({ title: "Accept failed", variant: "destructive" });
},
});
const activeRecord = useMemo(
() => records.find((r) => r.id === activeId) ?? null,
[records, activeId],
@@ -356,6 +388,38 @@ const FirstMilePage = () => {
[rowSelection],
);
const filteredPaidBookings = useMemo(() => {
const term = bookingSearch.trim().toLowerCase();
if (!term) return paidBookings;
return paidBookings.filter((b) =>
[b.reference, b.company?.name, b.company?.companyName]
.join(" ")
.toLowerCase()
.includes(term),
);
}, [paidBookings, bookingSearch]);
const openAccept = () => {
setAcceptOpen(true);
setAcceptStep(1);
setSelectedBooking(null);
setAcceptVehicleValue(null);
setBookingSearch("");
};
const closeAccept = () => {
setAcceptOpen(false);
setAcceptStep(1);
setSelectedBooking(null);
setAcceptVehicleValue(null);
setBookingSearch("");
};
const handleAcceptConfirm = () => {
if (!selectedBooking) return;
acceptMutation.mutate({ reference: selectedBooking.reference, vehicleId: acceptVehicleValue });
};
const matchesFilter = (r: FirstMileRecord) => {
switch (statusFilter) {
case "ALL": return true;
@@ -641,7 +705,7 @@ const FirstMilePage = () => {
Assign vehicle ({selectedIds.length})
</Button>
)}
<Button leftSection={<Truck size={16} />} onClick={() => openAssign(null)}>
<Button leftSection={<Truck size={16} />} onClick={openAccept}>
Assign Mile
</Button>
</Group>
@@ -773,6 +837,119 @@ const FirstMilePage = () => {
</Group>
</Stack>
</Modal>
{/* Accept Booking modal — step 1: booking list, step 2: details + vehicle */}
<Modal
opened={acceptOpen}
onClose={closeAccept}
title={
<Text fw={600}>
{acceptStep === 1 ? "Select a Paid Booking" : "Accept Booking"}
</Text>
}
size="xl"
radius="lg"
centered
>
{acceptStep === 1 ? (
<Stack gap="md">
<TextInput
placeholder="Search by reference or company…"
value={bookingSearch}
onChange={(e) => setBookingSearch(e.currentTarget.value)}
/>
<Stack gap="xs" style={{ maxHeight: 420, overflowY: "auto" }}>
{bookingsLoading ? (
<Text c="dimmed" size="sm" ta="center" py="md">Loading bookings</Text>
) : filteredPaidBookings.length === 0 ? (
<Text c="dimmed" size="sm" ta="center" py="md">No paid bookings found.</Text>
) : (
filteredPaidBookings.map((b) => (
<Card
key={b.id}
withBorder
padding="sm"
radius="md"
style={{ cursor: "pointer" }}
onClick={() => {
setSelectedBooking(b);
setAcceptStep(2);
}}
>
<Group justify="space-between" wrap="nowrap">
<Stack gap={2}>
<Text fw={600} size="sm">{b.reference}</Text>
<Text size="xs" c="dimmed">{b.company?.name ?? b.company?.companyName ?? "—"}</Text>
</Stack>
<Stack gap={2} align="flex-end">
<Text size="xs" c="dimmed">
{b.originYard?.name ?? "—"} {b.destinationYard?.name ?? "—"}
</Text>
<Text size="sm" fw={500}>{formatPrice(b.totalAmount)}</Text>
<Text size="xs" c="dimmed">
{b.scheduledDate ? b.scheduledDate.slice(0, 10) : "—"}
</Text>
</Stack>
</Group>
</Card>
))
)}
</Stack>
<Group justify="flex-end">
<Button variant="default" onClick={closeAccept}>Cancel</Button>
</Group>
</Stack>
) : (
<Stack gap="md">
{selectedBooking && (
<Card withBorder padding="md" radius="md" bg="var(--mantine-color-gray-0)">
<Stack gap="sm">
<Group justify="space-between">
<Text fw={600}>{selectedBooking.reference}</Text>
<Badge color="green" variant="light" size="sm">PAID</Badge>
</Group>
<SimpleGrid cols={2} spacing="sm">
<InfoRow label="Customer" value={selectedBooking.company?.name ?? selectedBooking.company?.companyName ?? "—"} />
<InfoRow label="Service type" value={selectedBooking.serviceType?.name ?? "—"} />
<InfoRow label="Pickup address" value={selectedBooking.firstMilePickupAddress ?? "—"} />
<InfoRow label="Origin yard" value={selectedBooking.originYard?.name ?? "—"} />
<InfoRow label="Destination yard" value={selectedBooking.destinationYard?.name ?? "—"} />
<InfoRow label="Cargo type" value={selectedBooking.cargoType?.name ?? "—"} />
<InfoRow label="Weight (VGM)" value={`${selectedBooking.cargoTotalWeightVgm} t`} />
<InfoRow label="Total amount" value={formatPrice(selectedBooking.totalAmount)} />
<InfoRow label="Contact person" value={selectedBooking.company?.contactPersonName ?? "—"} />
<InfoRow label="Contact phone" value={selectedBooking.company?.contactPersonPhone ?? selectedBooking.company?.phone ?? "—"} />
<InfoRow label="Requested date" value={selectedBooking.scheduledDate ? selectedBooking.scheduledDate.slice(0, 10) : "—"} />
</SimpleGrid>
</Stack>
</Card>
)}
<Divider />
<Select
label="Assign Vehicle (optional)"
placeholder="Select a vehicle"
data={vehicleOptions}
value={acceptVehicleValue}
onChange={setAcceptVehicleValue}
searchable
clearable
/>
<Group justify="space-between" gap="sm">
<Button variant="subtle" onClick={() => setAcceptStep(1)}> Back</Button>
<Group gap="sm">
<Button variant="default" onClick={closeAccept}>Cancel</Button>
<Button
onClick={handleAcceptConfirm}
loading={acceptMutation.isPending}
disabled={!selectedBooking}
>
Accept Booking
</Button>
</Group>
</Group>
</Stack>
)}
</Modal>
</Stack>
);
};