mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
fix first mile
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user