import React, { useEffect, useMemo, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useMutation } from '@tanstack/react-query'; import { HiOutlineArrowLeft } from 'react-icons/hi'; import { Button } from '@astryxdesign/core/Button'; import { IconButton } from '@astryxdesign/core/IconButton'; import { Tooltip } from '@astryxdesign/core/Tooltip'; import { Stack } from '@astryxdesign/core/Stack'; import { Heading } from '@astryxdesign/core/Heading'; import { finalCreatedeliveries, notifyRider } from '../api/api'; import { OpenToast } from 'components/nearle_components/OpenToast'; import CircularLoader from 'components/nearle_components/CircularLoader'; import Dispatch from './Dispatch'; import { BRAND, DT } from '../_shared/ordersDesign'; // Flatten the API's zoned shape into [{ rider_id, rider_name, orders }] for // the Reconcile tab UI and the reconcile-API payload. const extractRiders = (previewData) => { if (!previewData) return []; const map = new Map(); // De-dupe by orderid across the whole tree. A rider can legitimately appear // in multiple zones (one per delivery suburb), so the same rider_id is // visited more than once. Without this guard, any stale copy left behind // by applyReconcileResponse gets concatenated into the rider's orders and // the same orderid is sent twice to /deliveries/createdeliveries. const seenOrderIds = new Set(); const push = (riderId, riderName, orders) => { if (riderId == null) return; const key = String(riderId); if (!map.has(key)) { map.set(key, { rider_id: riderId, rider_name: riderName, orders: [] }); } const entry = map.get(key); (orders || []).forEach((o) => { const oid = o?.orderid != null ? String(o.orderid) : null; if (oid) { if (seenOrderIds.has(oid)) return; seenOrderIds.add(oid); } entry.orders.push(o); }); if (!entry.rider_name && riderName) entry.rider_name = riderName; }; if (Array.isArray(previewData.zones) && previewData.zones.length) { previewData.zones.forEach((z) => { (z.riders || []).forEach((r) => { const id = r.rider_id ?? r.userid; const name = r.rider_name || r.username || `Rider ${id}`; push(id, name, r.orders); }); }); } else if (Array.isArray(previewData.details)) { previewData.details.forEach((o) => { const id = o.rider_id ?? o.userid; const name = o.rider_name || o.ridername || `Rider ${id}`; push(id, name, [o]); }); } return Array.from(map.values()); }; // Reverse of extractRiders — flatten rider-grouped list into a details-style // array (used as the Assign Orders payload). const flattenRiders = (riders) => { const out = []; riders.forEach((r) => { // Go backend types Deliveries.userid as int — coerce here so any // upstream string (AI response, riders API, change-rider edit) gets // normalised before the JSON body is built. const ridNum = Number(r.rider_id); const rid = Number.isFinite(ridNum) ? ridNum : r.rider_id; (r.orders || []).forEach((o) => { out.push({ ...o, rider_id: rid, userid: rid, rider_name: r.rider_name, rider: r.rider_name }); }); }); return out; }; const Preview = () => { const navigate = useNavigate(); const location = useLocation(); const stateData = location.state || {}; // SINGLE SOURCE OF TRUTH: every Change Rider / Reconcile / Re-Assign goes // through this state. The Dispatch tab renders from it, the Reconcile tab // derives its rider list from it, and Assign Orders sends a flattened copy // of it to the API. const [dispatchPreviewData] = useState(stateData.dispatchPreviewData || null); // The AI response arrives via location.state, which the browser stores in // history.state and persists across reloads. That means a reload of // /dispatch/preview would re-hydrate the stale snapshot — including any // pending edits the user thought they discarded. Bounce to /orders when // there's no fresh response, and wipe the history snapshot once consumed // so a later reload / back-forward also bounces instead of re-using it. useEffect(() => { if (!stateData.dispatchPreviewData) { navigate('/nearle/orders', { replace: true }); return; } if (typeof window !== 'undefined' && window.history?.state) { window.history.replaceState({ ...window.history.state, usr: null }, ''); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { const prev = document.body.style.overflow; document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = prev; }; }, []); const [, setCsvExportData] = useState([]); const [isLoading, setIsLoading] = useState(false); const rider = stateData.rider || null; // Derived: rider list for the Reconcile tab. Recomputes whenever the cache // (dispatchPreviewData) changes — so Reconcile reflects here without a separate state. const reconcileRiders = useMemo(() => extractRiders(dispatchPreviewData), [dispatchPreviewData]); // Derived: flat orders list used for the Assign Orders payload + CSV export. // Always reflects the latest cache state. const finaldeliveryList = useMemo(() => { const flat = flattenRiders(reconcileRiders); if (flat.length) return computeDeliveryAmounts(flat); if (Array.isArray(dispatchPreviewData?.details)) { return computeDeliveryAmounts(dispatchPreviewData.details); } return []; }, [reconcileRiders, dispatchPreviewData]); useEffect(() => { const filtered = finaldeliveryList.map((item) => ({ zone_name: item.zone_name, ordernotes: item.ordernotes, rider: item.rider, step: item.step, ordertype: item.ordertype, orderamount: item.orderamount, riderkms: item.riderkms, cumulativekms: item.cumulativekms, baseprice: item.baseprice, minkm: item.minkm, priceperkm: item.priceperkm, kms: item.kms, actualkms: item.actualkms, rider_charge: item.rider_charge, deliveryamt: item.deliveryamt, deliverycharges: item.deliverycharges, profit: item.profit })); setCsvExportData(filtered); }, [finaldeliveryList]); const notifyRiderMutation = useMutation({ mutationFn: notifyRider, onSuccess: () => OpenToast('Notification sent Successfully', 'success', 2000), onError: (error) => OpenToast(error.message, 'error', 2000) }); const createFinalDeliveryMutation = useMutation({ mutationFn: finalCreatedeliveries, onSuccess: () => { OpenToast('Delivery Created Successfully', 'success', 2000); setIsLoading(false); if (rider?.userfcmtoken) notifyRiderMutation.mutate(rider.userfcmtoken); navigate('/nearle/deliveries'); }, onError: (error) => { OpenToast(error.message, 'error', 4000); setIsLoading(false); }, onSettled: () => setIsLoading(false) }); const handleFinalCreateDelivery = () => { if (!finaldeliveryList?.length) { OpenToast('No deliveries to assign', 'error', 3000); return; } setIsLoading(true); createFinalDeliveryMutation.mutate({ deliveries: finaldeliveryList }); }; return (