@@ -3674,36 +3724,88 @@ const Dispatch = ({
const statusStyle = getStatusStyle(o.orderstatus);
const profit = parseFloat(o.profit || 0);
const isLoss = profit < 0;
+
+ const orderRiderId = o.rider_id || o.userid;
+ const riderForOrder = orderRiderId ? riders.find(r => String(r.id) === String(orderRiderId)) : null;
+ const activeOrderId = (() => {
+ if (!riderForOrder) return null;
+ const sortedAll = [...riderForOrder.orders].sort((a, b) => {
+ const tA = a.trip_number || 1;
+ const tB = b.trip_number || 1;
+ if (tA !== tB) return tA - tB;
+ return (a.step || 0) - (b.step || 0);
+ });
+ const active = sortedAll.find((x) => {
+ const s = String(x.orderstatus || '').toLowerCase();
+ return !FINAL_STATUSES.has(s) && !SKIPPED_STATUSES.has(s);
+ });
+ return active ? active.orderid : null;
+ })();
+ const isGoingOn = activeOrderId && o.orderid === activeOrderId;
+ const estMeters = orderRiderId ? calculateEstMeters(orderRiderId, o) : null;
+
return (
setFocusedStop(isStopActive ? null : { orderid: o.orderid, lat, lon }) : undefined}
>
-
{idx + 1}
+
{o.step || idx + 1}
Order #{o.orderid}
{o.rider_name || o.ridername || 'Unassigned'}
- {o.orderstatus && (
-
- {statusStyle.label}
-
- )}
+ {(() => {
+ const actual = formatTimeOnly(o.deliverytime);
+ const expected = formatTimeOnly(o.expecteddeliverytime);
+ const isDelivered = FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase());
+ const showEstDrop = !isDelivered && estMeters !== null;
+ if (!o.orderstatus && !actual && !expected && !showEstDrop) return null;
+ return (
+
+ {o.orderstatus && (
+
+ {statusStyle.label}
+
+ )}
+ {(actual || expected) && (
+
+ {actual || expected}
+
+ )}
+ {showEstDrop && (
+
+ {formatMeters(estMeters)}
+
+ )}
+
+ );
+ })()}
{o.deliverycustomer || '—'}
+ {o.pickupcustomer && (
+
+ {o.pickupcustomer}
+
+ )}
{(o.deliverysuburb || o.deliveryaddress) && (
{o.deliverysuburb || extractArea(o.deliveryaddress)}
@@ -3733,7 +3835,7 @@ const Dispatch = ({
)}
- T{o.trip_number || '-'} · S{o.step || '-'}
+ T{o.trip_number || '-'} · S{o.step || idx + 1}
@@ -3767,17 +3869,37 @@ const Dispatch = ({
const statusStyle = getStatusStyle(o.orderstatus);
const profit = parseFloat(o.profit || 0);
const isLoss = profit < 0;
+
+ const orderRiderId = o.rider_id || o.userid;
+ const riderForOrder = orderRiderId ? riders.find(r => String(r.id) === String(orderRiderId)) : null;
+ const activeOrderId = (() => {
+ if (!riderForOrder) return null;
+ const sortedAll = [...riderForOrder.orders].sort((a, b) => {
+ const tA = a.trip_number || 1;
+ const tB = b.trip_number || 1;
+ if (tA !== tB) return tA - tB;
+ return (a.step || 0) - (b.step || 0);
+ });
+ const active = sortedAll.find((x) => {
+ const s = String(x.orderstatus || '').toLowerCase();
+ return !FINAL_STATUSES.has(s) && !SKIPPED_STATUSES.has(s);
+ });
+ return active ? active.orderid : null;
+ })();
+ const isGoingOn = activeOrderId && o.orderid === activeOrderId;
+ const estMeters = orderRiderId ? calculateEstMeters(orderRiderId, o) : null;
+
return (
setFocusedStop(isStopActive ? null : { orderid: o.orderid, lat, lon }) : undefined}
>
- {idx + 1}
+ {o.step || idx + 1}
Order #{o.orderid}
@@ -3785,14 +3907,41 @@ const Dispatch = ({
{o.rider_name || o.ridername || 'Unassigned'}
- {o.orderstatus && (
-
- {statusStyle.label}
-
- )}
+ {(() => {
+ const actual = formatTimeOnly(o.deliverytime);
+ const expected = formatTimeOnly(o.expecteddeliverytime);
+ const isDelivered = FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase());
+ const showEstDrop = !isDelivered && estMeters !== null;
+ if (!o.orderstatus && !actual && !expected && !showEstDrop) return null;
+ return (
+
+ {o.orderstatus && (
+
+ {statusStyle.label}
+
+ )}
+ {(actual || expected) && (
+
+ {actual || expected}
+
+ )}
+ {showEstDrop && (
+
+ {formatMeters(estMeters)}
+
+ )}
+
+ );
+ })()}
@@ -3833,7 +3982,7 @@ const Dispatch = ({
)}
- T{o.trip_number || '-'} · S{o.step || '-'}
+ T{o.trip_number || '-'} · S{o.step || idx + 1}
@@ -4029,77 +4178,6 @@ const Dispatch = ({
}
{renderMarkers()}
{renderRoutes()}
- {!focusedKitchen && riderPositions
- .filter(p => activeRiders.has(p.id))
- .filter(p => !focusedRider || focusedRider.id === p.id)
- .map(p => {
- // Prefer the road polyline midpoint; fall back to aerial midpoint until OSRM responds.
- const segPolyline = osrmRoutes[`${p.id}-seg-${p.nextStep}`];
- const roadMid = polylineMidpoint(segPolyline);
- const pos = roadMid || [p.aerialLat, p.aerialLon];
- const onRoad = Boolean(roadMid);
- const bikeIcon = L.divIcon({
- className: '',
- iconSize: [44, 44],
- iconAnchor: [22, 22],
- popupAnchor: [0, -22],
- html: `
@@ -4149,18 +4255,20 @@ const Dispatch = ({
LIVE GPS
- {r.status && (
-
- {r.status}
-
- )}
-
{r.username || `Rider #${r.id}`}
+
+ {r.username || `Rider #${r.id}`}
+ {r.status && (
+
+ {r.status}
+
+ )}
+
Rider ID: #{r.id}
@@ -4171,6 +4279,33 @@ const Dispatch = ({
#{r.orderid}
)}
+ {nextOrder && (
+
+ Next Stop
+
+ #{nextOrder.step || '?'} · {nextOrder.deliverycustomer || '—'}
+
+
+ )}
+ {nextDropArea && (
+
+ Next Location
+
+ {nextDropArea}
+
+
+ )}
+ {nextOrder?.pickupcustomer && (
+
+ Pickup
+
+ {nextOrder.pickupcustomer}
+
+
+ )}
{r.contactno && (
Phone
@@ -4254,6 +4389,15 @@ const Dispatch = ({
(o) => o.deliveryid != null && String(o.deliveryid) === String(t.deliveryid)
);
+ const statusStyle = getStatusStyle(t.orderstatus);
+ const flagSvg = t.orderstatus
+ ? `
+
+
+ ${isDelivered ? ' ' : ''}
+ `
+ : '';
+
const dropClasses = ['compare-step-pin'];
if (isFocusedStep) dropClasses.push('is-focused');
if (isDelivered) dropClasses.push('is-delivered');
@@ -4911,6 +5055,388 @@ const Dispatch = ({
)}
+ >)}
+
+ {!embedded && topView === 'analysis' && (
+
+
+ {ANALYSIS_BATCH_WINDOWS.map((b) => {
+ const result = analysisResults[b.key];
+ const isLoading = analysisLoadingWindow === b.key;
+ const hasError = result?.data?.success === false;
+ const statusBg = hasError ? '#fee2e2' : result ? `${b.color}22` : '#f1f5f9';
+ const statusFg = hasError ? '#dc2626' : result ? b.color : '#64748b';
+ const statusLabel = isLoading
+ ? 'Loading…'
+ : hasError
+ ? '! Failed'
+ : result
+ ? `✓ ${result.fetchedAt}`
+ : 'Fetch';
+ const isActive = activeBatchKey === b.key;
+ return (
+
!isLoading && handleFetchAnalysisBatch(b.key)}
+ style={{
+ borderColor: isActive ? b.color : hasError ? '#fecaca' : result ? b.border : '#e2e8f0',
+ background: hasError ? '#fef2f2' : result ? b.bg : '#ffffff',
+ boxShadow: isActive ? `0 0 0 2px ${b.color}33` : undefined
+ }}
+ disabled={isLoading}
+ >
+
+
+ {b.label[0]}
+
+
+
{b.label}
+
{b.timeRange}
+
+
+ {statusLabel}
+
+
+ {b.sub}
+
+ );
+ })}
+
+
+ {(() => {
+ if (!activeBatchKey) {
+ return (
+
+ Pick a batch above to view its efficiency analysis.
+
+ );
+ }
+ const activeMeta = ANALYSIS_BATCH_WINDOWS.find((b) => b.key === activeBatchKey);
+ const cached = analysisResults[activeBatchKey];
+ const isLoading = analysisLoadingWindow === activeBatchKey;
+
+ if (isLoading && !cached) {
+ return (
+
Loading {activeMeta.label} batch…
+ );
+ }
+ if (!cached) return null;
+
+ const raw = cached.data || {};
+ if (raw.success === false) {
+ return (
+
+
+
+
{activeMeta.label} Batch
+
+ {activeMeta.timeRange} · Fetched at {cached.fetchedAt}
+
+
+
handleFetchAnalysisBatch(activeBatchKey)}
+ disabled={isLoading}
+ style={{ background: '#fee2e2', color: '#dc2626' }}
+ >
+
+
+
+
+
+
+ {raw?.error?.code || 'Request failed'}
+
+
+ {raw?.error?.message || 'The server returned an error.'}
+
+ {raw?.request_id && (
+
request_id: {raw.request_id}
+ )}
+
+
+ );
+ }
+
+ const fleet = raw.fleet_summary || {};
+ const riders = Array.isArray(raw.rider_timelines) ? raw.rider_timelines : [];
+ const subs = Array.isArray(raw.substitution_opportunities) ? raw.substitution_opportunities : [];
+ const rec = raw.top_recommendation;
+ const win = raw.window || {};
+
+ const fleetMetrics = [
+ { label: 'Total Orders', value: analysisFormatNum(fleet.total_orders) },
+ { label: 'Total Riders', value: analysisFormatNum(fleet.total_riders) },
+ { label: 'Avg Orders/Rider', value: fleet.orders_per_rider_avg ?? '—' },
+ { label: 'Fleet Start', value: fleet.fleet_start || '—' },
+ { label: 'Fleet Done', value: fleet.fleet_done || '—' },
+ { label: 'Duration', value: fleet.total_duration_minutes != null ? `${fleet.total_duration_minutes} min` : '—' }
+ ];
+
+ return (
+
+
+
+
+ {activeMeta.label} Batch
+
+ {raw.date ? ` · ${raw.date}` : ''}
+ {win.from && win.to ? ` · ${win.from} – ${win.to}` : ''}
+
+
+
+ Fetched at {cached.fetchedAt} · Input deliveries: {raw.input_delivery_count ?? '—'}
+
+
+
{
+ // Force refetch (bypass cache)
+ setAnalysisResults((prev) => {
+ const next = { ...prev };
+ delete next[activeBatchKey];
+ return next;
+ });
+ batchEfficiencyMutation.mutate({
+ batch: activeBatchKey,
+ tenantId: ANALYSIS_TENANT_ID
+ });
+ }}
+ disabled={isLoading}
+ style={{ background: `${activeMeta.color}22`, color: activeMeta.color }}
+ >
+
+
+
+
+
+
Fleet Summary
+
+ {fleetMetrics.map((m) => (
+
+
{m.label}
+
{m.value}
+
+ ))}
+
+
+
+ {rec && (
+
+
Top Recommendation
+
+
+
+
+ {(rec.action || 'recommendation').replaceAll('_', ' ')}
+
+ {rec.fleet_improvement_minutes != null && (
+
0
+ ? { background: '#dcfce7', color: '#166534' }
+ : { background: '#f1f5f9', color: '#475569' }
+ }
+ >
+ {rec.fleet_improvement_minutes > 0 ? '↑' : '•'} Fleet improves by {rec.fleet_improvement_minutes} min
+
+ )}
+
+
+ {rec.idle_rider_name || `Rider ${rec.idle_rider_id}`}
+ {rec.primary_kitchen && (
+ <> · primary kitchen {rec.primary_kitchen} >
+ )}
+ {rec.second_kitchen && (
+ <> → also serve {rec.second_kitchen} after {rec.second_kitchen_dispatch_after || '—'}>
+ )}
+
+ {rec.description && (
+
{rec.description}
+ )}
+ {rec.activate_when?.rules?.length > 0 && (
+
+
+ Activate when ({rec.activate_when.condition || 'AND'}):
+
+ {rec.activate_when.rules.map((rule, i) => (
+
+ {rule.field} {rule.operator} {rule.value}
+ {rule.reason && — {rule.reason} }
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {riders.length > 0 && (
+
+
+ Rider Timelines ({riders.length})
+
+
+ {riders.map((r) => {
+ const isActive = String(r.status || '').toLowerCase() === 'active';
+ return (
+
+
+
+
+ {r.name}
+ #{r.userid}
+
+
+ {r.status}
+
+
+
+ {r.kitchen && (
+
+ {r.kitchen}
+
+ )}
+
+ {r.order_count} orders
+
+
+ {r.started_at} → {r.finished_at}
+
+ 30
+ ? { background: '#fef3c7', color: '#92400e' }
+ : undefined
+ }
+ >
+ {r.idle_minutes} min idle
+
+
+
+ );
+ })}
+
+
+ )}
+
+ {subs.length > 0 && (
+
+
+ Substitution Opportunities ({subs.length})
+
+
+ {subs.map((s, i) => {
+ const idle = s.idle_rider || {};
+ const relieved = s.most_relieved_rider || {};
+ const improved = s.fleet_improvement_minutes ?? 0;
+ return (
+
+
+
+ {idle.name || `Rider ${idle.userid}`} {' '}
+ covers {s.target_kitchen}
+
+
0
+ ? { background: '#dcfce7', color: '#166534' }
+ : { background: '#f1f5f9', color: '#475569' }
+ }
+ >
+ Fleet {improved > 0 ? '↑' : '•'} {improved} min
+
+
+
+
+ {s.travel_to_kitchen_km} km
+
+
+ {s.travel_to_kitchen_minutes} min travel
+
+
+ arrives {s.arrive_at_kitchen}
+
+
+ {s.total_orders_transferred} orders
+
+
+ +{s.extra_km_for_idle_rider} km for idle rider
+
+
+ {relieved.name && (
+
+
+ Most relieved: {relieved.name} {' '}
+ ({relieved.original_finish} → {relieved.new_finish}, saves{' '}
+ {relieved.time_saved_minutes} min)
+
+ )}
+ {Array.isArray(s.orders_to_transfer) && s.orders_to_transfer.length > 0 && (
+
+
Orders transferred
+ {s.orders_to_transfer.map((o) => {
+ const imp = o.improvement_minutes ?? 0;
+ return (
+
+ #{o.deliveryid}
+
+ from {o.from_rider_name}
+
+
+ {o.original_delivery_time} → {o.estimated_delivery_time}
+
+ 0
+ ? { background: '#dcfce7', color: '#166534' }
+ : imp < 0
+ ? { background: '#fee2e2', color: '#991b1b' }
+ : { background: '#f1f5f9', color: '#475569' }
+ }
+ >
+ {imp > 0 ? '+' : ''}{imp} min
+
+
+ );
+ })}
+
+ )}
+
+ );
+ })}
+
+
+ )}
+
+ );
+ })()}
+
+ )}
);
diff --git a/src/pages/nearle/dispatch/Preview.js b/src/pages/nearle/dispatch/Preview.js
new file mode 100644
index 0000000..07c168b
--- /dev/null
+++ b/src/pages/nearle/dispatch/Preview.js
@@ -0,0 +1,717 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { useLocation, useNavigate } from 'react-router-dom';
+import {
+ Autocomplete,
+ Backdrop,
+ Box,
+ Button,
+ Card,
+ Chip,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ IconButton,
+ Stack,
+ Tab,
+ Tabs,
+ TextField,
+ Tooltip,
+ Typography
+} from '@mui/material';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import ArrowBackIcon from '@mui/icons-material/ArrowBack';
+import { HiOutlineArrowLeft } from 'react-icons/hi';
+import { IoReload } from 'react-icons/io5';
+import { MdTwoWheeler, MdSwapHoriz } from 'react-icons/md';
+
+import {
+ createAutomationDeliveries,
+ createOptimisationDeliveries,
+ fetchRidersList,
+ finalCreatedeliveries,
+ notifyRider,
+ reconcileSteps
+} from '../../api/api';
+import { OpenToast } from 'components/third-party/OpenToast';
+import CSVExport from 'components/third-party/ReactTable';
+import CircularLoader from 'components/CircularLoader';
+import Dispatch from './Dispatch';
+import { stepColor } from './dispatchShared';
+
+const tuningTypes = [
+ { tuneid: 1, type: 'Balanced', value: 'balanced' },
+ { tuneid: 2, type: 'Aggressive Speed', value: 'aggressive_speed' },
+ { tuneid: 3, type: 'Fuel Saver', value: 'fuel_saver' },
+ { tuneid: 4, type: 'Zone Strict', value: 'zone_strict' }
+];
+
+// 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();
+ 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);
+ entry.orders.push(...(orders || []));
+ 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) => {
+ (r.orders || []).forEach((o) => {
+ out.push({
+ ...o,
+ rider_id: r.rider_id,
+ userid: r.rider_id,
+ rider_name: r.rider_name,
+ rider: r.rider_name
+ });
+ });
+ });
+ return out;
+};
+
+// Move one order from oldRiderId -> newRiderId inside dispatchPreviewData.
+// Mutates both the zones[].riders[].orders[] tree (so the Dispatch tab
+// renders the change) AND the flat details[] list (so Assign Orders picks
+// it up). Returns a NEW preview object (immutable update).
+const moveOrderInPreviewData = (preview, { orderId, newRiderId, newRiderName }) => {
+ if (!preview) return preview;
+ const next = JSON.parse(JSON.stringify(preview));
+
+ // 1) Update flat details list
+ if (Array.isArray(next.details)) {
+ next.details = next.details.map((o) =>
+ String(o.orderid) === String(orderId)
+ ? { ...o, rider_id: newRiderId, userid: newRiderId, rider_name: newRiderName, rider: newRiderName }
+ : o
+ );
+ }
+
+ // 2) Move within zones[].riders[].orders[]
+ if (Array.isArray(next.zones)) {
+ let movedOrder = null;
+ let homeZoneIdx = -1;
+
+ for (let zi = 0; zi < next.zones.length && !movedOrder; zi++) {
+ const zone = next.zones[zi];
+ if (!Array.isArray(zone.riders)) continue;
+ for (let ri = 0; ri < zone.riders.length && !movedOrder; ri++) {
+ const r = zone.riders[ri];
+ if (!Array.isArray(r.orders)) continue;
+ const oi = r.orders.findIndex((o) => String(o.orderid) === String(orderId));
+ if (oi !== -1) {
+ movedOrder = r.orders[oi];
+ r.orders.splice(oi, 1);
+ homeZoneIdx = zi;
+ }
+ }
+ }
+
+ if (movedOrder) {
+ const updated = {
+ ...movedOrder,
+ rider_id: newRiderId,
+ userid: newRiderId,
+ rider_name: newRiderName,
+ rider: newRiderName
+ };
+ let placed = false;
+ for (const zone of next.zones) {
+ if (!Array.isArray(zone.riders)) continue;
+ const target = zone.riders.find(
+ (r) => String(r.rider_id ?? r.userid) === String(newRiderId)
+ );
+ if (target) {
+ target.orders = target.orders || [];
+ target.orders.push(updated);
+ placed = true;
+ break;
+ }
+ }
+ if (!placed && homeZoneIdx >= 0) {
+ next.zones[homeZoneIdx].riders.push({
+ rider_id: newRiderId,
+ userid: newRiderId,
+ rider_name: newRiderName,
+ orders: [updated]
+ });
+ }
+ }
+ }
+
+ return next;
+};
+
+// Merge a reconcile-API response { riders:[{rider_id, orders}] } back into
+// dispatchPreviewData. Replaces each rider's orders[] in zones (preserving
+// zone containment), then rebuilds the flat details list from the new tree.
+const applyReconcileResponse = (preview, response) => {
+ if (!preview || !Array.isArray(response?.riders)) return preview;
+ const next = JSON.parse(JSON.stringify(preview));
+
+ const newOrdersByRider = new Map(
+ response.riders.map((r) => [String(r.rider_id), r.orders || []])
+ );
+
+ if (Array.isArray(next.zones) && next.zones.length) {
+ next.zones.forEach((zone) => {
+ if (!Array.isArray(zone.riders)) return;
+ zone.riders.forEach((r) => {
+ const key = String(r.rider_id ?? r.userid);
+ if (newOrdersByRider.has(key)) {
+ r.orders = newOrdersByRider.get(key);
+ newOrdersByRider.delete(key);
+ } else {
+ // Rider wasn't in the response — keep their existing orders untouched.
+ }
+ });
+ });
+ // Any response riders we didn't place attach to the first zone.
+ if (newOrdersByRider.size) {
+ const target = next.zones[0];
+ target.riders = target.riders || [];
+ newOrdersByRider.forEach((orders, riderKey) => {
+ target.riders.push({
+ rider_id: Number(riderKey) || riderKey,
+ rider_name: orders[0]?.rider_name || `Rider ${riderKey}`,
+ orders
+ });
+ });
+ }
+ } else {
+ next.zones = [
+ {
+ zone_name: 'Reconciled',
+ riders: response.riders.map((r) => ({
+ rider_id: r.rider_id,
+ rider_name: r.rider_name || `Rider ${r.rider_id}`,
+ orders: r.orders || []
+ }))
+ }
+ ];
+ }
+
+ // Rebuild flat details from the updated zones->riders->orders tree.
+ const flatDetails = [];
+ next.zones.forEach((zone) => {
+ (zone.riders || []).forEach((r) => {
+ (r.orders || []).forEach((o) => {
+ flatDetails.push({
+ ...o,
+ rider_id: r.rider_id,
+ userid: r.rider_id,
+ rider_name: r.rider_name,
+ rider: r.rider_name
+ });
+ });
+ });
+ });
+ next.details = flatDetails;
+
+ return next;
+};
+
+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, setDispatchPreviewData] = useState(stateData.dispatchPreviewData || null);
+ const [csvExportData, setCsvExportData] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [tabValue, setTabValue] = useState(0);
+
+ const [reconcileLoading, setReconcileLoading] = useState(false);
+ const [hasReconciled, setHasReconciled] = useState(false);
+
+ // Change-rider dialog state
+ const [changeDialogOpen, setChangeDialogOpen] = useState(false);
+ const [selectedOrder, setSelectedOrder] = useState(null);
+ const [selectedOldRiderId, setSelectedOldRiderId] = useState(null);
+ const [selectedNewRider, setSelectedNewRider] = useState(null);
+
+ const aiMode = stateData.aiMode ?? 1;
+ const selectedMode = stateData.selectedMode || null;
+ const deliveryData = stateData.deliveryData || [];
+ const autoRiders = stateData.autoRiders || [];
+ const absentRidersPayload = stateData.absentRidersPayload || [];
+ const rider = stateData.rider || null;
+
+ const appId = useMemo(() => {
+ if (stateData.appId) return stateData.appId;
+ if (typeof window !== 'undefined') {
+ const v = localStorage.getItem('applocationid');
+ return v ? Number(v) : 0;
+ }
+ return 0;
+ }, [stateData.appId]);
+
+ const { data: ridersList } = useQuery({
+ queryKey: ['ridersList', appId],
+ queryFn: fetchRidersList,
+ enabled: !!appId,
+ staleTime: 5 * 60 * 1000
+ });
+
+ // Derived: rider list for the Reconcile tab. Recomputes whenever the cache
+ // (dispatchPreviewData) changes — so Change Rider / Reconcile both reflect
+ // 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 createDeliveryMutation = useMutation({
+ mutationFn: aiMode == 0 ? createOptimisationDeliveries : createAutomationDeliveries,
+ onSuccess: (data) => {
+ OpenToast('Orders Optimised Successfully', 'success', 2000);
+ // Brand new response = brand new source of truth.
+ setDispatchPreviewData(data);
+ setHasReconciled(false);
+ setIsLoading(false);
+ },
+ onError: (error) => {
+ OpenToast(error.message, 'error', 4000);
+ setIsLoading(false);
+ },
+ onSettled: () => setIsLoading(false)
+ });
+
+ 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 reconcileMutation = useMutation({
+ mutationFn: reconcileSteps,
+ onMutate: () => setReconcileLoading(true),
+ onSuccess: (data) => {
+ if (Array.isArray(data?.riders)) {
+ setDispatchPreviewData((prev) => applyReconcileResponse(prev, data));
+ setHasReconciled(true);
+ OpenToast('Steps reconciled — preview updated', 'success', 2000);
+ } else {
+ OpenToast('Reconcile returned no rider data', 'warning', 3000);
+ }
+ },
+ onError: (error) => {
+ OpenToast(error.message || 'Reconcile failed', 'error', 4000);
+ },
+ onSettled: () => setReconcileLoading(false)
+ });
+
+ const handleCreateDelivery = (tune) => {
+ setIsLoading(true);
+ if (aiMode == 0) {
+ createDeliveryMutation.mutate({ deliveries: deliveryData });
+ } else if (selectedMode && selectedMode?.value == 1) {
+ createDeliveryMutation.mutate({
+ deliveries: deliveryData,
+ hypertuning_params: tune || null,
+ selectedMode,
+ absent_riders: absentRidersPayload
+ });
+ } else {
+ createDeliveryMutation.mutate({
+ data: {
+ orders: deliveryData,
+ riders: autoRiders,
+ config: { pay_type: 'hourly', base_pay: 300.0, strategy: 'multi_trip' },
+ absent_riders: absentRidersPayload
+ },
+ selectedMode
+ });
+ }
+ };
+
+ const handleFinalCreateDelivery = () => {
+ if (!finaldeliveryList?.length) {
+ OpenToast('No deliveries to assign', 'error', 3000);
+ return;
+ }
+ setIsLoading(true);
+ createFinalDeliveryMutation.mutate({ deliveries: finaldeliveryList });
+ };
+
+ const handleReconcile = () => {
+ if (!reconcileRiders.length) {
+ OpenToast('No riders to reconcile', 'warning', 3000);
+ return;
+ }
+ // Payload built straight from the cache — whatever Change Rider edited
+ // is what gets sent.
+ reconcileMutation.mutate({
+ riders: reconcileRiders.map((r) => ({
+ rider_id: r.rider_id,
+ orders: r.orders
+ }))
+ });
+ };
+
+ const openChangeRider = (oldRider, order) => {
+ const oldId =
+ oldRider?.rider_id ?? oldRider?.id ?? order?.rider_id ?? order?.userid ?? null;
+ setSelectedOldRiderId(oldId);
+ setSelectedOrder(order);
+ setSelectedNewRider(null);
+ setChangeDialogOpen(true);
+ };
+
+ const confirmChangeRider = () => {
+ if (!selectedNewRider || !selectedOrder) return;
+ const newRiderId = selectedNewRider.userid;
+ const newRiderName =
+ selectedNewRider.label ||
+ `${selectedNewRider.firstname || ''} ${selectedNewRider.lastname || ''}`.trim() ||
+ `Rider ${newRiderId}`;
+
+ setDispatchPreviewData((prev) =>
+ moveOrderInPreviewData(prev, {
+ orderId: selectedOrder.orderid,
+ oldRiderId: selectedOldRiderId,
+ newRiderId,
+ newRiderName
+ })
+ );
+ setHasReconciled(false);
+ setChangeDialogOpen(false);
+ OpenToast('Rider changed — click Reconcile to verify steps', 'info', 2500);
+ };
+
+ return (
+
+ theme.zIndex.modal + 1 }}
+ open={isLoading}
+ >
+
+
+
+
+
+
+
+ navigate('/nearle/orders')}
+ sx={{ bgcolor: 'action.hover', '&:hover': { bgcolor: 'action.selected' } }}
+ >
+
+
+
+
+ Assign Orders
+
+
+
+ option.type}
+ sx={{ minWidth: 250, maxWidth: 600, flex: 1 }}
+ renderInput={(params) => }
+ onChange={(e, val, reason) => {
+ if (reason === 'clear') handleCreateDelivery(null);
+ else handleCreateDelivery(val.value);
+ }}
+ />
+ }
+ onClick={() => {
+ setIsLoading(true);
+ handleCreateDelivery('reshuffle');
+ }}
+ >
+ Re-Assign
+
+
+
+
+
+
+
+ setTabValue(v)} sx={{ minHeight: 40 }}>
+
+
+
+
+
+
+ {tabValue === 0 && dispatchPreviewData && (
+ openChangeRider(focusedRider, order)}
+ />
+ )}
+ {tabValue === 1 && (
+
+ {reconcileRiders.length === 0 ? (
+
+ No rider data available to reconcile.
+
+ ) : (
+
+
+ {hasReconciled
+ ? 'Steps have been reconciled. The Dispatch tab and Assign payload are updated.'
+ : 'Click a numbered step to change its rider. Hit Reconcile to verify the corrected steps with the server.'}
+
+
+ {reconcileRiders.map((r) => {
+ const totalKms = r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0);
+ return (
+
+
+
+
+
+
+
+
+ {r.rider_name}
+
+
+ ID: {r.rider_id}
+
+
+
+
+
+
+
+
+
+
+ {r.orders.map((o, idx) => {
+ const stepNum = o.step ?? idx + 1;
+ const color = stepColor(Number(stepNum) - 1);
+ return (
+
+ Order #{o.orderid}
+ {o.deliveryaddress || o.deliverysuburb || ''}
+ Click to change rider
+
+ }
+ >
+ openChangeRider(r, o)}
+ sx={{
+ width: 36,
+ height: 36,
+ borderRadius: '50%',
+ bgcolor: color,
+ color: '#fff',
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ fontWeight: 800,
+ fontSize: 14,
+ cursor: 'pointer',
+ boxShadow:
+ '0 0 0 2px rgba(255,255,255,0.6), 0 1px 3px rgba(15,23,42,0.15)',
+ transition: 'transform 0.15s',
+ '&:hover': { transform: 'scale(1.08)' }
+ }}
+ >
+ {stepNum}
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+
+ }
+ onClick={handleReconcile}
+ disabled={reconcileLoading}
+ sx={{ minWidth: 220, borderRadius: '10px', textTransform: 'none', fontWeight: 700 }}
+ >
+ {reconcileLoading ? 'Reconciling...' : 'Reconcile'}
+
+
+
+ )}
+
+ )}
+
+
+
+
+ }
+ onClick={() => navigate(-1)}
+ >
+ Back
+
+
+ Assign Orders
+
+
+
+
+ setChangeDialogOpen(false)} maxWidth="xs" fullWidth>
+ Change Rider
+
+
+ Move order #{selectedOrder?.orderid} (step {selectedOrder?.step ?? '—'}) to:
+
+
+ o?.label || `${o?.firstname || ''} ${o?.lastname || ''}`.trim() || ''
+ }
+ value={selectedNewRider}
+ onChange={(e, val) => setSelectedNewRider(val)}
+ renderInput={(params) => }
+ />
+
+
+ setChangeDialogOpen(false)}>Cancel
+
+ Change Rider
+
+
+
+
+ );
+};
+
+// Mirrors the orders.js deliveryamt recalc — applied at render-time so the
+// Assign payload always reflects the current cache without a useEffect.
+function computeDeliveryAmounts(list) {
+ return list.map((item) => {
+ const cumulativeKms = Number(item.cumulativekms || 0);
+ const minKm = Number(item.minkm || 0);
+ const basePrice = Number(item.baseprice || 0);
+ const pricePerKm = Number(item.priceperkm || 0);
+ if (cumulativeKms <= minKm) return { ...item, deliveryamt: basePrice };
+ return { ...item, deliveryamt: (cumulativeKms - minKm) * pricePerKm + basePrice };
+ });
+}
+
+export default Preview;
diff --git a/src/pages/nearle/orders/OrdersRedesign.css b/src/pages/nearle/orders/OrdersRedesign.css
index 875087c..0302fa0 100644
--- a/src/pages/nearle/orders/OrdersRedesign.css
+++ b/src/pages/nearle/orders/OrdersRedesign.css
@@ -1096,13 +1096,11 @@
}
.gradient-btn-create:hover {
- transform: translateY(-1px) !important;
filter: brightness(1.04);
box-shadow: 0 8px 18px -4px rgba(24, 144, 255, 0.40), 0 3px 8px rgba(101, 56, 122, 0.18) !important;
}
.gradient-btn-create:active {
- transform: translateY(0) !important;
filter: brightness(0.98);
}
diff --git a/src/pages/nearle/orders/multipleOrders.js b/src/pages/nearle/orders/multipleOrders.js
index 147f814..fbca5e0 100644
--- a/src/pages/nearle/orders/multipleOrders.js
+++ b/src/pages/nearle/orders/multipleOrders.js
@@ -1,23 +1,11 @@
-import React from 'react';
-import Loader from 'components/Loader';
-import { useEffect, useState, Fragment, useRef } from 'react';
-import { useTheme } from '@mui/material/styles';
-import MainCard from 'components/MainCard';
+import React, { useEffect, useState, useRef, Fragment } from 'react';
import axios from 'axios';
-import ClearIcon from '@mui/icons-material/Clear';
-import { SearchOutlined, CloseOutlined, ExclamationCircleOutlined, FileAddOutlined } from '@ant-design/icons';
-import { Empty } from 'antd';
-import MyLocationIcon from '@mui/icons-material/MyLocation';
-import { DatePicker } from '@mui/x-date-pickers/DatePicker';
-import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
-import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
-import dayjs from 'dayjs';
-var utc = require('dayjs/plugin/utc');
-dayjs.extend(utc);
-import { enqueueSnackbar } from 'notistack';
-import { useNavigate } from 'react-router';
import Papa from 'papaparse';
import * as XLSX from 'xlsx';
+import dayjs from 'dayjs';
+import { useNavigate } from 'react-router';
+import { useTheme } from '@mui/material/styles';
+import { enqueueSnackbar } from 'notistack';
import {
FormControl,
@@ -26,6 +14,7 @@ import {
Typography,
Stack,
Box,
+ Card,
Button,
TextField,
Autocomplete,
@@ -47,12 +36,54 @@ import {
TableRow,
Paper,
TableHead,
- FormLabel,
- RadioGroup,
- Radio,
- Backdrop
+ Backdrop,
+ Chip,
+ Tooltip
} from '@mui/material';
+import { DatePicker } from '@mui/x-date-pickers/DatePicker';
+import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
+import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
+import ClearIcon from '@mui/icons-material/Clear';
+import MyLocationIcon from '@mui/icons-material/MyLocation';
+import {
+ SearchOutlined,
+ CloseOutlined,
+ ExclamationCircleOutlined,
+ FileAddOutlined,
+ CalendarOutlined,
+ ClockCircleOutlined,
+ FileTextOutlined,
+ InboxOutlined,
+ LockOutlined,
+ CheckCircleFilled
+} from '@ant-design/icons';
+import { Empty } from 'antd';
+import { FaUser, FaTruck, FaUsers, FaPaperPlane, FaRoute, FaMoneyBillWave, FaBoxes, FaReceipt } from 'react-icons/fa';
+import { FaLocationDot } from 'react-icons/fa6';
+import { MdOutlineCloudUpload } from 'react-icons/md';
+
+import Loader from 'components/Loader';
import CircularLoader from 'components/CircularLoader';
+import AnimateButton from 'components/@extended/AnimateButton';
+import './OrdersRedesign.css';
+
+var utc = require('dayjs/plugin/utc');
+dayjs.extend(utc);
+
+// ============================== small style tokens ==============================
+const cellHeaderSx = {
+ fontSize: 11.5,
+ fontWeight: 700,
+ color: '#475569',
+ py: 0.75,
+ px: 1
+};
+
+const cellBodySx = {
+ fontSize: 12,
+ py: 0.6,
+ px: 1
+};
const MultipleOrders = () => {
const navigate = useNavigate();
@@ -60,544 +91,357 @@ const MultipleOrders = () => {
const locationRef = useRef(null);
const tenantRef = useRef(null);
const userid = localStorage.getItem('userid');
+
+ // ============================== state ==============================
const [locations, setLocations] = useState([]);
const [tenantlist, setTenantlist] = useState([]);
+ const [tenantLocations, setTenantlocations] = useState([]);
const [loading, setLoading] = useState(false);
const [btnLoading, setBtnLoading] = useState(false);
+
const [appId, setAppId] = useState(0);
- const [tenantLocations, setTenantlocations] = useState([]);
const [tenantid, setTenantid] = useState(0);
const [locationid, setLocationid] = useState(0);
+ const [tenantValue, setTenantValue] = useState(null);
+ const [locationValue, setLocationValue] = useState(null);
+
const [basePrice, setBasePrice] = useState(0);
const [pricePerKm, setPricePerKm] = useState(0);
const [minKm, setMinKm] = useState(0);
+
const [pickCust, setPickCust] = useState(null);
const [dropCust, setDropCust] = useState([]);
+ const [customerlist, setCustomerlist] = useState([]);
const [isCustomerOpen, setIsCustomerOpen] = useState(false);
const [searchCustList, setSearchCustList] = useState('');
- const [customerlist, setCustomerlist] = useState([]);
- const [timeslotarr, setTimeslotarr] = useState([]);
+
const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY'));
const [selectedtime, setSelectedtime] = useState('');
- const [alertmessage, setAlertmessage] = useState('');
+ const [pickupSlotsList, setPickupSlotsList] = useState(null);
+ const [pickupSlot, setPickupSlot] = useState(null);
+
const [otherinstructions, setOtherinstructions] = useState('');
const [admintoken, setAdmintoken] = useState();
+
const [totaldist, settotaldist] = useState(0);
const [totalAmt, settotalAmt] = useState(0);
const [totalQty, settotalQty] = useState(0);
const [totalCash, settotalCash] = useState(0);
- const [users, setUsers] = useState([]);
+
const [uploadType, setUploadType] = useState(null);
- const [tenantValue, setTenantValue] = useState(null);
- const [locationValue, setLocationValue] = useState(null);
- const [pickupSlotsList, setPickupSlotsList] = useState(null);
- const [pickupSlot, setPickupSlot] = useState(null);
+ const [users, setUsers] = useState([]);
+ const [fileName, setFileName] = useState('');
- // to clear the tenant and location autocomplete
- useEffect(() => {
- setTenantid(0);
- setTenantValue(null);
- setLocationid(0);
- setLocationValue(null);
- }, [appId]);
- // to clear the location autocomplete
- useEffect(() => {
- setLocationid(0);
- setLocationValue(null);
- }, [tenantid]);
+ // Stable dedup cache for OpenToast. Was a `let` inside the component body,
+ // which got recreated on every render and broke the dedup. A ref persists
+ // across renders without triggering re-renders itself.
+ const toastCacheRef = useRef({});
- useEffect(() => {
- if (timeslotarr[0]) {
- let arr = [];
- timeslotarr.map((val) => {
- if (dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0) {
- arr.push(val);
- }
- });
- }
- }, [timeslotarr]);
-
- // =============================================== || opentoast || ===============================================
+ // ============================== toast ==============================
const opentoast = (message, variant, time) => {
enqueueSnackbar(message, {
variant: variant,
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: time ? time : 1500
});
- console.log(alertmessage);
};
- // 🔹 Smart toast wrapper — prevents duplicate toasts for same message within 3 seconds
- let toastCache = {};
- const OpenToast = (message, type = 'info', timeout = 10000) => {
+ const OpenToast = (message, type = 'info', timeout = 3000) => {
const key = `${type}-${message}`;
- if (toastCache[key]) return; // skip duplicates
- opentoast(message, type, timeout); // your existing toast/snackbar
- toastCache[key] = true;
- setTimeout(() => delete toastCache[key], 3000); // reset after delay
+ if (toastCacheRef.current[key]) return;
+ opentoast(message, type, timeout);
+ toastCacheRef.current[key] = true;
+ setTimeout(() => delete toastCacheRef.current[key], 3000);
};
- // ==============================|| fetchAppLocations ||============================== //
+ // ============================== effects: reset chains ==============================
+ useEffect(() => {
+ // appId change → clear downstream selections (tenant, location, customers)
+ setTenantid(0);
+ setTenantValue(null);
+ setLocationid(0);
+ setLocationValue(null);
+ setTenantlocations([]);
+ setPickCust(null);
+ setDropCust([]);
+ setUsers([]);
+ setUploadType(null);
+ setFileName('');
+ setPickupSlotsList(null);
+ setPickupSlot(null);
+ setSelectedtime('');
+ }, [appId]);
+ useEffect(() => {
+ // tenantid change → clear location/customers (keep appId)
+ setLocationid(0);
+ setLocationValue(null);
+ setDropCust([]);
+ setUsers([]);
+ setUploadType(null);
+ setFileName('');
+ }, [tenantid]);
+
+ // ============================== fetchAppLocations ==============================
const fetchAppLocations = async () => {
setLoading(true);
-
try {
const locationRes = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`);
- console.log('fetchAppLocations', locationRes.data.details);
- setLocations(locationRes.data.details);
+ setLocations(locationRes.data.details || []);
} catch (err) {
- console.log('locationRes', err);
OpenToast(err.message, 'error', 5000);
} finally {
setLoading(false);
}
};
+
useEffect(() => {
fetchAppLocations();
}, []);
- // ===================================================== || fetchtenantinfolist || =====================================================
-
+ // ============================== fetchtenantinfolist ==============================
const fetchtenantinfolist = async () => {
setLoading(true);
- await axios
- .get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${appId}&status=active`)
-
- .then((res) => {
- console.log(res);
- if (res.data.status) {
- let arr = [];
- res.data.details.map((val) => {
- arr.push({
- ...val,
- label: `${val.tenantname}`
- });
- });
- setTenantlist(arr);
- }
- setLoading(false);
- })
- .catch((err) => {
- console.log(err);
- setLoading(false);
- });
- };
- useEffect(() => {
- appId && fetchtenantinfolist();
- }, [appId]);
- // ============================================= || fetchTenantPricing || =============================================
-
- const fetchTenantPricing = async (id) => {
try {
- const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`);
- console.log('pricingResponse', pricingResponse.data.details);
- setBasePrice(pricingResponse.data.details.baseprice);
- setPricePerKm(pricingResponse.data.details.priceperkm);
- setMinKm(pricingResponse.data.details.minkm);
- } catch (error) {
- console.log('fetchTenantPricing error', error);
- }
- };
- // ============================================= || gettenantlocations (branches) || =============================================
- const gettenantlocations = async (id) => {
- try {
- const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
- console.log('gettenantlocations', res.data.details);
- if (res.data.details.length == 1) {
- setTenantlocations(res.data.details);
- setPickCust(res.data.details[0]);
- setLocationid(res.data.details[0].locationid);
- setLocationValue(res.data.details[0].locationid);
- setPickupSlotsList(res.data.details[0].slots);
- } else {
- setTenantlocations(res.data.details);
+ const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${appId}&status=active`);
+ if (res.data.status) {
+ const arr = (res.data.details || []).map((val) => ({ ...val, label: `${val.tenantname}` }));
+ setTenantlist(arr);
}
} catch (err) {
- console.log('gettenantlocations', err);
- }
- };
- // ========================================================= || clientdetails || =========================================================
- const clientdetails = async () => {
- try {
- let url =
- searchCustList == ''
- ? `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tenantid}&pageno=1&pagesize=30`
- : `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tenantid}&keyword=${searchCustList}`;
- await axios
- .get(url)
- .then((res) => {
- if (res.data.status) {
- console.log('clientdetails', res.data.details);
-
- setCustomerlist(res.data.details);
- let arr = [];
- res.data.details.map((val) => {
- arr.push({
- label: `${val.firstname} | ${val.contactno}`,
- ...val
- });
- });
- }
- })
- .catch((err) => {
- console.log(err);
- opentoast('server error', 'warning');
- });
- } catch (err) {
- console.log(err);
- }
- };
- useEffect(() => {
- if (tenantid) {
- clientdetails();
- }
- }, [searchCustList.length > 3, searchCustList == '', tenantid]);
-
- // ========================================================= || calculateTotal(dist , charge) || =========================================================
- const calculateTotal = () => {
- let a1 = 0;
- let a2 = 0;
- let a3 = 0;
- let a4 = 0;
- dropCust.map((customer) => {
- a1 += customer.distance;
- a2 += customer.totalcharge;
- a3 += customer.quantity;
- a4 += customer.collectionamt;
- });
- settotaldist(a1);
- settotalAmt(a2);
- settotalQty(a3);
- settotalCash(a4);
- };
- useEffect(() => {
- calculateTotal();
- }, [dropCust]);
-
- // ========================================================= || handleCheckboxChange || =========================================================
- const handleCheckboxChange = async (event, customer) => {
- setLoading(true);
- if (event.target.checked) {
- // If the checkbox is checked, calculate the distance and add the customer
- try {
- const obj = await calculateDistance(customer);
- const { roundedDistance, totalcharge } = obj;
- // Create a new customer object with the distance property
- const updatedCustomer = {
- ...customer,
- distance: roundedDistance,
- totalcharge: totalcharge
- };
-
- // Add the updated customer object to dropCust
- setDropCust((prevDropCust) => [...prevDropCust, updatedCustomer]);
-
- // Log the rounded distance
- // console.log(`Rounded Distance: ${roundedDistance} km`);
- } catch (error) {
- console.error('Failed to calculate distance:', error);
- } finally {
- setLoading(false);
- }
- } else {
- // If the checkbox is unchecked, remove the customer from dropCust
- setDropCust((prevDropCust) => {
- return prevDropCust.filter((cust) => cust.customerid !== customer.customerid);
- });
- setLoading(false);
- }
- };
- // ========================================================= || handleCheckboxChange1 || =========================================================
- // const handleCheckboxChange1 = async (customer) => {
- // console.log('customer', customer);
- // setLoading(true);
- // try {
- // const obj = await calculateDistance(customer);
- // const { roundedDistance, totalcharge } = obj;
- // // Create a new customer object with the distance property
- // const updatedCustomer = {
- // ...customer,
- // distance: roundedDistance,
- // totalcharge: totalcharge
- // };
-
- // // Add the updated customer object to dropCust
- // setDropCust((prevDropCust) => [...prevDropCust, updatedCustomer]);
-
- // // Log the rounded distance
- // console.log(`Rounded Distance: ${roundedDistance} km`);
- // setLoading(false);
- // } catch (error) {
- // console.error('Failed to calculate distance:', error);
- // }
- // };
- const handleCheckboxChange1 = async (customer) => {
- console.log('customer', customer);
-
- setLoading(true);
-
- try {
- setDropCust((prevDropCust) => {
- const isAlreadySelected = prevDropCust.some((c) => c.firstname === customer.firstname);
-
- // 🔴 REMOVE if already exists
- if (isAlreadySelected) {
- return prevDropCust.filter((c) => c.firstname !== customer.firstname);
- }
-
- // 🟢 ADD if not exists (calculate distance)
- return prevDropCust;
- });
-
- // Only calculate distance if customer is not already added
- const alreadyExists = dropCust.some((c) => c.firstname === customer.firstname);
-
- if (!alreadyExists) {
- const obj = await calculateDistance(customer);
- const { roundedDistance, totalcharge } = obj;
-
- const updatedCustomer = {
- ...customer,
- distance: roundedDistance,
- totalcharge
- };
-
- setDropCust((prevDropCust) => [...prevDropCust, updatedCustomer]);
-
- console.log(`Rounded Distance: ${roundedDistance} km`);
- }
- } catch (error) {
- console.error('Failed to calculate distance:', error);
+ OpenToast('Failed to load clients', 'warning', 3000);
} finally {
setLoading(false);
}
};
- // ========================================================= || calculateDistance || =========================================================
+ useEffect(() => {
+ if (appId) fetchtenantinfolist();
+ }, [appId]);
- // 🔹 Main distance calculation function
+ // ============================== fetchTenantPricing ==============================
+ const fetchTenantPricing = async (id) => {
+ try {
+ const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`);
+ const d = res.data.details || {};
+ setBasePrice(d.baseprice || 0);
+ setPricePerKm(d.priceperkm || 0);
+ setMinKm(d.minkm || 0);
+ } catch (error) {
+ console.log('fetchTenantPricing error', error);
+ }
+ };
+
+ // ============================== gettenantlocations ==============================
+ const gettenantlocations = async (id) => {
+ try {
+ const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
+ const details = res.data.details || [];
+ if (details.length === 1) {
+ setTenantlocations(details);
+ setPickCust(details[0]);
+ setLocationid(details[0].locationid);
+ setLocationValue(details[0]);
+ setPickupSlotsList(details[0].slots);
+ } else {
+ setTenantlocations(details);
+ }
+ } catch (err) {
+ console.log('gettenantlocations', err);
+ }
+ };
+
+ // ============================== clientdetails ==============================
+ const clientdetails = async () => {
+ try {
+ const url =
+ searchCustList === ''
+ ? `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tenantid}&pageno=1&pagesize=30`
+ : `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tenantid}&keyword=${searchCustList}`;
+ const res = await axios.get(url);
+ if (res.data.status) setCustomerlist(res.data.details || []);
+ } catch (err) {
+ console.log(err);
+ opentoast('server error', 'warning');
+ }
+ };
+
+ useEffect(() => {
+ if (!tenantid) return;
+ // Light debounce so we don't fire on every keystroke.
+ const t = setTimeout(() => {
+ if (searchCustList === '' || searchCustList.length > 2) clientdetails();
+ }, 250);
+ return () => clearTimeout(t);
+ }, [searchCustList, tenantid]);
+
+ // ============================== totals ==============================
+ useEffect(() => {
+ let a1 = 0;
+ let a2 = 0;
+ let a3 = 0;
+ let a4 = 0;
+ dropCust.forEach((c) => {
+ a1 += Number(c.distance) || 0;
+ a2 += Number(c.totalcharge) || 0;
+ a3 += Number(c.quantity) || 0;
+ a4 += Number(c.collectionamt) || 0;
+ });
+ settotaldist(a1);
+ settotalAmt(a2);
+ settotalQty(a3);
+ settotalCash(a4);
+ }, [dropCust]);
+
+ // ============================== distance (Google) ==============================
const calculateDistance = async (customer) => {
- const service = new google.maps.DistanceMatrixService();
+ if (typeof window === 'undefined' || !window.google?.maps?.DistanceMatrixService) {
+ throw new Error('Google Maps not loaded');
+ }
+ const service = new window.google.maps.DistanceMatrixService();
- // Helper: safely get distance matrix
- const getDistanceMatrix = (origins, destinations) => {
- return new Promise((resolve, reject) => {
- // 2;
+ const getDistanceMatrix = (origins, destinations) =>
+ new Promise((resolve, reject) => {
try {
- if (!origins || !destinations) {
- return reject(new Error('Origin or destination data missing.'));
- }
+ if (!origins || !destinations) return reject(new Error('Origin or destination data missing.'));
service.getDistanceMatrix(
{
- origins: [new google.maps.LatLng(origins.latitude, origins.longitude)],
- destinations: [new google.maps.LatLng(destinations.latitude, destinations.longitude)],
+ origins: [new window.google.maps.LatLng(origins.latitude, origins.longitude)],
+ destinations: [new window.google.maps.LatLng(destinations.latitude, destinations.longitude)],
travelMode: 'DRIVING',
- unitSystem: google.maps.UnitSystem.METRIC
+ unitSystem: window.google.maps.UnitSystem.METRIC
},
(response, status) => {
- if (status === 'OK') {
- resolve(response);
- } else {
- reject(new Error(`Google API error: ${status}`));
- }
+ if (status === 'OK') resolve(response);
+ else reject(new Error(`Google API error: ${status}`));
}
);
} catch (err) {
reject(new Error(`Unexpected error inside DistanceMatrixService: ${err.message}`));
}
});
- };
try {
- // --- Input validation ---
- if (!customer || typeof customer !== 'object') {
- throw new Error('Invalid customer data: expected an object.');
- }
+ if (!customer || typeof customer !== 'object') throw new Error('Invalid customer data.');
+ if (!pickCust || typeof pickCust !== 'object') throw new Error('Origin (pickCust) data missing or invalid.');
- if (!pickCust || typeof pickCust !== 'object') {
- throw new Error('Origin (pickCust) data missing or invalid.');
- }
-
- // --- Call Google Maps API ---
const response = await getDistanceMatrix(pickCust, customer);
+ const distVal = response?.rows?.[0]?.elements?.[0]?.distance?.value;
+ if (distVal == null) throw new Error('Malformed Distance Matrix response: missing distance value.');
- // --- Validate response structure ---
- if (!response.rows?.[0]?.elements?.[0] || !response.rows[0].elements[0].distance?.value) {
- throw new Error('Malformed Distance Matrix response: missing distance value.');
- }
- // --- Compute distance ---
- const distanceInMeters = response.rows[0].elements[0].distance.value;
- const distanceInKilometers = distanceInMeters / 1000;
- const roundedDistance = Math.round(distanceInKilometers);
-
- // --- Calculate total charge ---
- let totalcharge;
- if (roundedDistance < minKm) {
- totalcharge = basePrice;
- } else {
- totalcharge = (roundedDistance - minKm) * pricePerKm + basePrice;
- }
+ const km = distVal / 1000;
+ const roundedDistance = Math.round(km);
+ const totalcharge =
+ roundedDistance < minKm ? basePrice : (roundedDistance - minKm) * pricePerKm + basePrice;
return { roundedDistance, totalcharge };
} catch (error) {
- // --- Categorized smart error handling ---
- console.log('on calculateDistance', error.message);
if (error.message.includes('Google API')) {
- console.log('🚨 Google Maps API Error:', error.message);
- OpenToast('Invalid file format, upload valid file', 'error', 5000);
- } else if (error.message.includes('Invalid coordinates')) {
- console.log('📍 Invalid coordinate format:', error.message, 3000);
- OpenToast('Invalid coordinate format. Check location data.', 'warning'), 3000;
+ OpenToast('Invalid coordinates or Google API error.', 'error', 3000);
} else if (error.message.includes('Malformed Distance Matrix')) {
- console.log('⚠️ Unexpected Google response structure:', error.message);
OpenToast('Google Distance Matrix returned invalid data.', 'error', 3000);
} else if (error.message.includes('Origin') || error.message.includes('customer')) {
- console.log('❌ Missing or invalid input data:', error.message);
OpenToast('Missing or invalid input data for distance calculation.', 'warning', 3000);
} else {
- console.log('💥 Unexpected error calculating distance:', error);
OpenToast('Unexpected error during distance calculation.', 'error', 3000);
}
-
- throw error; // keeps your current flow intact
+ throw error;
}
};
- // ==================================================== || fetchTiming || ====================================================
- const fetchTiming = async () => {
- await axios
- .get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`)
- .then((res) => {
- console.log('fetchTiming', res);
- const { opentime, closetime } = res.data.details[0];
- if (res.data.status) {
- console.log('starttime', `${dayjs().format('MM-DD-YYYY')} ${opentime}`);
- console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `);
- let arr = [];
- for (
- let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`;
- dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
- i = dayjs(i).add(30, 'm')
- ) {
- arr.push(i);
- }
- console.log('setTimeslotarr', arr);
- setTimeslotarr(arr);
- }
- setLoading(false);
- })
- .catch((err) => {
- console.log(err);
- setLoading(false);
+ // ============================== handleCheckboxChange (dialog: add/remove on tick) ==============================
+ const handleCheckboxChange = async (event, customer) => {
+ setLoading(true);
+ try {
+ if (event.target.checked) {
+ const { roundedDistance, totalcharge } = await calculateDistance(customer);
+ setDropCust((prev) => [...prev, { ...customer, distance: roundedDistance, totalcharge }]);
+ } else {
+ setDropCust((prev) => prev.filter((c) => c.customerid !== customer.customerid));
+ }
+ } catch (err) {
+ console.error('Failed to calculate distance:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Toggle handler used by:
+ // 1. CSV "Continue" bulk add (matches by firstname since uploaded rows
+ // don't carry a stable customerid).
+ // 2. Per-row remove (CloseOutlined) in the drop table.
+ // Uses a single functional updater + locally-captured already-selected flag
+ // so we don't read stale `dropCust` after the state change.
+ const handleCheckboxChange1 = async (customer) => {
+ // Compute "already selected" from the latest state synchronously.
+ let wasSelected = false;
+ setDropCust((prev) => {
+ wasSelected = prev.some((c) => c.firstname === customer.firstname);
+ if (wasSelected) {
+ return prev.filter((c) => c.firstname !== customer.firstname);
+ }
+ return prev;
+ });
+
+ if (wasSelected) return; // remove path is done
+
+ // Add path — compute distance, then append.
+ setLoading(true);
+ try {
+ const { roundedDistance, totalcharge } = await calculateDistance(customer);
+ setDropCust((prev) => {
+ // Guard against parallel adds for the same row.
+ if (prev.some((c) => c.firstname === customer.firstname)) return prev;
+ return [...prev, { ...customer, distance: roundedDistance, totalcharge }];
});
- };
- useEffect(() => {
- if (appId) {
- fetchTiming();
+ } catch (err) {
+ console.error('Failed to calculate distance:', err);
+ } finally {
+ setLoading(false);
}
- }, [appId]);
+ };
+ // ============================== fetchAppAdminTokens ==============================
const fetchAppAdminTokens = async () => {
- setLoading(true);
- await axios
- .get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`)
- .then((res) => {
- const userfcmtokemArray = res.data.details.applocationadmins.map((admin) => admin.userfcmtokem); // fcm => firebase cloud messaging
- console.log('fetchAppAdminTokens', res);
- console.log('userfcmtokemArray', userfcmtokemArray);
- if (res.data.status) {
- setAdmintoken(userfcmtokemArray);
- }
- setLoading(false);
- })
- .catch((err) => {
- console.log(err);
- setLoading(false);
- });
+ try {
+ const res = await axios.get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`);
+ if (res.data.status) {
+ const tokens = res.data.details.applocationadmins.map((a) => a.userfcmtokem);
+ setAdmintoken(tokens);
+ }
+ } catch (err) {
+ console.log(err);
+ }
};
useEffect(() => {
- if (appId) {
- fetchAppAdminTokens();
- }
+ if (appId) fetchAppAdminTokens();
}, [appId]);
- useEffect(() => {
- console.log('pickCust', pickCust);
- }, [pickCust]);
- useEffect(() => {
- console.log('dropCust', dropCust);
- }, [dropCust]);
- // ==================================================== || fetchtenantinfo || ====================================================
- const fetchtenantinfo = async () => {
- setLoading(true);
- console.log('tenantid', tenantid);
-
- await axios
- .get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tenantid}`)
- .then((res) => {
- console.log('fetchtenantinfo', res);
- if (res.data.status) {
- setTenantid(res.data.details.tenantid);
- }
- setLoading(false);
- })
- .catch((err) => {
- console.log(err);
- setLoading(false);
- });
- };
- useEffect(() => {
- if (tenantid) {
- fetchtenantinfo();
- }
- }, [tenantid]);
- // ================================================== || sendnotifications || ==================================================
+ // ============================== sendnotifications ==============================
const sendnotifications = async () => {
- setLoading(true);
- await axios
- .post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, {
+ try {
+ const res = await axios.post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, {
priority: 'high',
registration_ids: admintoken,
- data: {
- accessid: process.env.REACT_APP_RIDER_ACCESS_ID
- },
+ data: { accessid: process.env.REACT_APP_RIDER_ACCESS_ID },
notification: {
title: 'Nearle Merchant',
- body: 'An Order has been placed successfully,kindly process the same',
+ body: 'An Order has been placed successfully, kindly process the same',
sound: 'ring'
}
- })
- .then((res) => {
- console.log(res);
- if (res.data.message == 'Success') {
- enqueueSnackbar('Notification sent Successfully', {
- variant: 'success',
- anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 1000
- });
- }
- setLoading(false);
- })
- .catch((err) => {
- console.log(err);
- enqueueSnackbar(err.message, {
- variant: 'error',
- anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 1000
- });
- setLoading(false);
});
+ if (res.data.message === 'Success') {
+ opentoast('Notification sent Successfully', 'success', 1000);
+ }
+ } catch (err) {
+ opentoast(err.message, 'error', 1000);
+ }
};
- const cleanReceiverName = (name) => {
- if (typeof name !== 'string') return name;
- return name.replace(/^[\d.\s]+/, '').trim();
- };
+ // ============================== CSV / XLSX upload ==============================
+ const cleanReceiverName = (name) => (typeof name === 'string' ? name.replace(/^[\d.\s]+/, '').trim() : name);
+ const normalizeHeader = (header) => header?.toString().trim().toLowerCase().replace(/\s+/g, '');
-
-
- // your header mapping
const headerMap = {
'pickupdate(yyyy-mmm-dd)': 'date',
'sendername*': 'locationname',
@@ -614,9 +458,6 @@ const MultipleOrders = () => {
' Collect Cash': 'collectionamt'
};
- // helper to normalize headers
- const normalizeHeader = (header) => header?.toString().trim().toLowerCase().replace(/\s+/g, '');
-
const handleFileDirectUpload = (event) => {
try {
const file = event.target.files?.[0];
@@ -624,10 +465,9 @@ const MultipleOrders = () => {
opentoast('No file selected.', 'warning');
return;
}
-
- const fileName = file.name.toLowerCase();
- const isCSV = fileName.endsWith('.csv');
- const isExcel = fileName.endsWith('.xls') || fileName.endsWith('.xlsx');
+ const fileNameLower = file.name.toLowerCase();
+ const isCSV = fileNameLower.endsWith('.csv');
+ const isExcel = fileNameLower.endsWith('.xls') || fileNameLower.endsWith('.xlsx');
if (!isCSV && !isExcel) {
opentoast('Invalid file type. Please upload a CSV or Excel file.', 'warning');
@@ -635,12 +475,8 @@ const MultipleOrders = () => {
}
const processData = (data, headers) => {
- console.log('data', data);
const normalizedMap = {};
- for (const key in headerMap) {
- normalizedMap[normalizeHeader(key)] = headerMap[key];
- }
- console.log('normalizedMap', normalizedMap);
+ for (const key in headerMap) normalizedMap[normalizeHeader(key)] = headerMap[key];
const mappedData = data.map((row) => {
const newRow = {};
@@ -649,26 +485,23 @@ const MultipleOrders = () => {
const newKey = normalizedMap[cleanKey] || cleanKey;
let value = row[key];
if (newKey === 'firstname') value = cleanReceiverName(value);
-
newRow[newKey] = value;
}
-
return newRow;
});
- const missingCols = Object.keys(headerMap).filter((clientCol) => !headers.includes(normalizeHeader(clientCol)));
-
+ const missingCols = Object.keys(headerMap).filter(
+ (clientCol) => !headers.includes(normalizeHeader(clientCol))
+ );
if (missingCols.length > 0) {
- isExcel && opentoast(`Missing columns: ${missingCols.join(', ')}`, 'warning');
+ opentoast(`Missing columns: ${missingCols.join(', ')}`, 'warning');
}
- console.log('✅ Final Processed Data:', mappedData);
setUsers(mappedData);
- opentoast('File uploaded and successfully ', 'success', 3000);
- opentoast('Press Continue', 'warning', 3000);
+ opentoast('File uploaded successfully', 'success', 2000);
+ opentoast('Press Continue to add as drop customers', 'info', 2500);
};
- // ============ CSV handler ============
if (isCSV) {
Papa.parse(file, {
header: true,
@@ -683,59 +516,92 @@ const MultipleOrders = () => {
const headers = results.meta.fields.map(normalizeHeader);
processData(results.data, headers);
},
- error: (error) => {
- console.error('❌ CSV Parsing Error:', error);
- opentoast(`CSV parsing failed: ${error.message}`, 'warning');
- }
+ error: (error) => opentoast(`CSV parsing failed: ${error.message}`, 'warning')
});
}
- // ============ Excel handler ============
if (isExcel) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = e.target.result;
- // Try reading as binary first
- let workbook;
- try {
- workbook = XLSX.read(data, { type: 'binary' });
- } catch {
- // fallback for modern XLSX files
- const arrayBuffer = new Uint8Array(data);
- workbook = XLSX.read(arrayBuffer, { type: 'array' });
- }
-
+ const workbook = XLSX.read(data, { type: 'binary' });
const firstSheet = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheet];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { defval: '' });
-
if (!jsonData?.length) {
opentoast('Excel file is empty or invalid.', 'warning');
setUsers([]);
return;
}
-
const headers = Object.keys(jsonData[0]).map(normalizeHeader);
processData(jsonData, headers);
} catch (err) {
- console.error('❌ Error processing Excel:', err);
opentoast(`Error reading Excel: ${err.message}`, 'warning');
}
};
-
- // Important: use readAsBinaryString for Excel
reader.readAsBinaryString(file);
}
} catch (err) {
- console.error('Unexpected error during file upload:', err);
opentoast(`Unexpected error: ${err.message}`, 'warning');
}
};
- // =============================================== || createorders || ===============================================
+ const removeFileExtension = (n) => n.replace(/\.[^/.]+$/, '');
+ const onFileChange = (event) => {
+ const file = event.target.files[0];
+ if (!file) return;
+ const cleanedName = removeFileExtension(file.name);
+ setFileName((prev) => (prev ? `${prev}, ${cleanedName}` : cleanedName));
+ handleFileDirectUpload(event);
+ };
+
+ // ============================== row editors ==============================
+ const handleQuantityChange = (customerid, value) => {
+ setDropCust((prev) => prev.map((c) => (c.customerid === customerid ? { ...c, quantity: Number(value) || 0 } : c)));
+ };
+ const handleCollectionAmtChange = (customerid, value) => {
+ setDropCust((prev) => prev.map((c) => (c.customerid === customerid ? { ...c, collectionamt: Number(value) || 0 } : c)));
+ };
+
+ // ============================== createorders ==============================
+ const buildDeliveryTime = () => {
+ // Prefer the parsed pickupSlot (already merged date + slot's time).
+ // Fall back to startdate + selectedtime when present.
+ if (pickupSlot) {
+ const parsed = dayjs(pickupSlot, ['YYYY-MM-DD hh:mm A', 'YYYY-MM-DD HH:mm:ss']);
+ if (parsed.isValid()) return parsed.format('YYYY-MM-DD HH:mm:ss');
+ }
+ if (startdate && selectedtime) {
+ const parsed = dayjs(`${dayjs(startdate).format('YYYY-MM-DD')} ${selectedtime}`, [
+ 'YYYY-MM-DD hh:mm A',
+ 'YYYY-MM-DD HH:mm:ss'
+ ]);
+ if (parsed.isValid()) return parsed.format('YYYY-MM-DD HH:mm:ss');
+ }
+ return dayjs().format('YYYY-MM-DD HH:mm:ss');
+ };
+
const createorders = async () => {
- // ===================== Build Payload =====================
+ if (!tenantid) {
+ opentoast('Choose Client', 'warning');
+ return;
+ }
+ if (!pickCust) {
+ opentoast('Pickup location required', 'warning');
+ return;
+ }
+ if (!pickupSlot) {
+ opentoast('Select a pickup slot', 'warning');
+ return;
+ }
+ if (!dropCust.length) {
+ opentoast('Add at least one drop customer', 'warning');
+ return;
+ }
+
+ const deliverytime = buildDeliveryTime();
+
const arr = dropCust.map((customer) => ({
applocationid: pickCust.applocationid,
configid: 9,
@@ -766,7 +632,7 @@ const MultipleOrders = () => {
deliverylocation: customer.suburb || '',
deliverylocationid: customer.deliverylocationid || 0,
deliverylong: customer.longitude?.toString() || '',
- deliverytime: `${dayjs(startdate).format('YYYY-MM-DD')} ${dayjs(selectedtime.$d).format('HH:mm:ss')}`,
+ deliverytime,
deliverytype: 'B',
itemcount: 1,
@@ -785,836 +651,1269 @@ const MultipleOrders = () => {
pickupSlot
}));
- console.log('arr', arr);
-
- // ===================== Validation =====================
- if (!tenantid) {
- opentoast('Choose Client', 'warning');
- return;
- }
setLoading(true);
+ setBtnLoading(true);
try {
const res = await axios.post(`${process.env.REACT_APP_URL}/orders/createorders`, arr);
if (res.data.status) {
- opentoast('Order Created Successfully', 'success', 2000);
- if (admintoken) {
- sendnotifications();
- }
+ opentoast('Orders Created Successfully', 'success', 2000);
+ if (admintoken) sendnotifications();
navigate('/nearle/orders');
- setLoading(false);
} else {
- console.log(res.data);
- console.error('Create order failed (API response):', res.data);
opentoast(res?.data?.message || 'Order creation failed. Please try again.', 'warning', 3000);
}
} catch (err) {
- opentoast(err.message, 'error', 2000);
- console.log('create orders', err.message);
- console.error('Create order error:', {
- message: err.message,
- response: err.response,
- request: err.request,
- stack: err.stack
- });
-
- // Exact but short error for user
let toastMessage = 'Something went wrong. Please try again.';
-
if (err.response) {
- // Server responded with error
toastMessage = err.response.data?.message || `Server error (${err.response.status})`;
} else if (err.request) {
- // No response received
toastMessage = 'Network error. Check your internet connection.';
}
- opentoast(toastMessage, 'error');
- setLoading(false);
+ opentoast(toastMessage, 'error', 3000);
} finally {
setLoading(false);
setBtnLoading(false);
}
};
- const [fileName, setFileName] = useState('');
- const removeFileExtension = (fileName) => {
- return fileName.replace(/\.[^/.]+$/, '');
+ // ============================== derived ==============================
+ const stepsComplete = {
+ location: !!appId,
+ client: !!tenantid,
+ business: !!locationid,
+ schedule: !!pickupSlot,
+ drops: dropCust.length > 0
};
+ const canSubmit =
+ stepsComplete.location && stepsComplete.client && stepsComplete.business && stepsComplete.schedule && stepsComplete.drops;
- const onFileChange = (event) => {
- const file = event.target.files[0];
- if (!file) return;
- const cleanedName = removeFileExtension(file.name);
- setFileName((prev) => (prev ? `${prev}, ${cleanedName}` : cleanedName));
- if (tenantid === 916) {
- handleFileDirectUpload(event);
- } else {
- handleFileDirectUpload(event);
- // handleFileUpload(event);
- }
- };
-
- const handleQuantityChange = (customerid, value) => {
- setDropCust((prev) => prev.map((cust) => (cust.customerid === customerid ? { ...cust, quantity: Number(value) || 0 } : cust)));
- };
- const handleCollectionAmtChange = (customerid, value) => {
- setDropCust((prev) => prev.map((cust) => (cust.customerid === customerid ? { ...cust, collectionamt: Number(value) || 0 } : cust)));
- };
+ // ============================== preview helpers ==============================
+ // The right pane shows three exclusive views:
+ // 1. dropCust.length > 0 → "Drop List" with calculated charges + remove
+ // 2. users.length > 0 → raw "File Preview" of parsed rows, awaiting Continue
+ // 3. else → empty state
+ const previewMode = dropCust.length > 0 ? 'drops' : users.length > 0 ? 'preview' : 'empty';
+ // ============================== render ==============================
return (
<>
- {loading && (
- <>
-
- {/* */}
- >
- )}
- {
- theme.zIndex.drawer + 1
- }}
- open={btnLoading} // when loader = true, backdrop covers the page
- >
-
-
- }
+ {loading && }
+ t.zIndex.drawer + 1 }} open={btnLoading}>
+
+
-
-
-
- Create Multiple Order
-
-
-
-
-
- {/* ===================================================== || Choose App location || ===================================================== */}
-
- `${option.locationname}`}
- onChange={(event, value, reason) => {
- if (reason === 'clear') {
- setAppId(0);
- setTenantid(0);
- setTenantValue(null);
- setTenantlist([]);
- setLocationid(0);
- setLocationValue(null);
- setTenantlocations([]);
- setPickCust(null);
- setDropCust([]);
- setUploadType(null);
- } else {
- setAppId(value.applocationid);
- setDropCust([]);
- setPickCust(null);
- }
- }}
- renderInput={(params) => }
- />
-
- {/* ===================================================== || Choose client || ===================================================== */}
-
- option?.tenantname || ''}
- isOptionEqualToValue={(option, value) => option.tenantid === value.tenantid}
- renderOption={(props, option) => (
-
- {option.tenantname}
-
- )}
- onOpen={(event) => {
- if (!appId) {
- event.preventDefault();
- OpenToast('Please select your location first!', 'warning', 3000);
- setTimeout(() => {
- locationRef.current?.focus();
- }, 0);
- }
- }}
- onChange={(e, val, reason) => {
- if (reason === 'clear') {
- setTenantid(0);
- setTenantValue(null);
- setLocationid(0);
- setLocationValue(null);
- setTenantlocations([]);
- setPickCust(null);
- setDropCust([]);
- setUploadType(null);
- } else if (val) {
- setTenantid(val.tenantid);
- setTenantValue(val);
- setLocationid(0);
- setLocationValue(null);
- fetchTenantPricing(val.tenantid);
- gettenantlocations(val.tenantid);
- setDropCust([]);
- }
- }}
- renderInput={(params) => }
- />
-
- {/* ===================================================== ||Business Location || ===================================================== */}
-
- {tenantLocations?.length == 1 ? (
-
-
-
- )
- }}
- />
- ) : (
+ {/* Thin title bar */}
+
+
+ Create Multiple Orders
+
+
+
+
+ Bulk-create deliveries from CSV/Excel or saved customers.
+
+
+
+ {/* ============================== 50 / 50 workspace ============================== */}
+
+ {/* ============================== LEFT 50% : Input fields ============================== */}
+
+ {/* Card: Setup (Location / Client / Business) */}
+
+
+
+ Setup
+
+
+
+
`${option.locationname} (${option.suburb})` || ''}
- value={locationValue}
- onOpen={(event) => {
- if (!appId && !tenantid) {
- event.preventDefault();
- OpenToast('Please select Location and Tenant first!', 'warning', 3000);
- setTimeout(() => {
- locationRef.current?.focus();
- }, 0);
- } else if (!tenantid) {
- event.preventDefault();
- OpenToast('Please select Tenant first!', 'warning', 3000);
- setTimeout(() => {
- tenantRef.current?.focus();
- }, 0);
- }
- }}
+ size="small"
+ ref={locationRef}
+ className="header-compact-input"
+ options={locations || []}
+ getOptionLabel={(option) => `${option.locationname}`}
onChange={(event, value, reason) => {
- if (reason === 'clear') {
- setLocationid(0);
- setLocationValue(null);
- setPickCust(null);
- } else {
- setLocationid(value.locationid || 0);
- setLocationValue(value);
- setPickCust(value);
- setPickupSlotsList(value?.slots);
+ if (reason === 'clear') setAppId(0);
+ else if (value) setAppId(value.applocationid);
+ }}
+ renderInput={(params) => (
+
+
+ {params.InputProps.startAdornment}
+ >
+ )
+ }}
+ />
+ )}
+ />
+
+
+ option?.tenantname || ''}
+ isOptionEqualToValue={(option, value) => option.tenantid === value.tenantid}
+ onOpen={(event) => {
+ if (!appId) {
+ event.preventDefault();
+ OpenToast('Please select Location first!', 'warning', 3000);
+ setTimeout(() => locationRef.current?.focus(), 0);
}
}}
- renderInput={(params) => }
+ onChange={(e, val, reason) => {
+ if (reason === 'clear') {
+ setTenantid(0);
+ setTenantValue(null);
+ } else if (val) {
+ setTenantid(val.tenantid);
+ setTenantValue(val);
+ fetchTenantPricing(val.tenantid);
+ gettenantlocations(val.tenantid);
+ }
+ }}
+ renderInput={(params) => (
+
+
+ {params.InputProps.startAdornment}
+ >
+ )
+ }}
+ />
+ )}
/>
- )}
+
+
+ {tenantLocations.length === 1 ? (
+
+ )
+ }}
+ />
+ ) : (
+
+ option?.locationname ? `${option.locationname} (${option.suburb || ''})` : ''
+ }
+ onOpen={(event) => {
+ if (!appId && !tenantid) {
+ event.preventDefault();
+ OpenToast('Please select Location and Client first!', 'warning', 3000);
+ setTimeout(() => locationRef.current?.focus(), 0);
+ } else if (!tenantid) {
+ event.preventDefault();
+ OpenToast('Please select Client first!', 'warning', 3000);
+ setTimeout(() => tenantRef.current?.focus(), 0);
+ }
+ }}
+ onChange={(event, value, reason) => {
+ if (reason === 'clear' || !value) {
+ setLocationid(0);
+ setLocationValue(null);
+ setPickCust(null);
+ setPickupSlotsList(null);
+ } else {
+ setLocationid(value.locationid || 0);
+ setLocationValue(value);
+ setPickCust(value);
+ setPickupSlotsList(value?.slots);
+ }
+ }}
+ renderInput={(params) => (
+
+
+ {params.InputProps.startAdornment}
+ >
+ )
+ }}
+ />
+ )}
+ />
+ )}
+
-
-
-
- {/* ===================================================== || Pickup || ===================================================== */}
-
-
- {locationid !== 0 && (
-
-
+
+
+ {/* Card: Schedule + Pickup display */}
+
+
+
+ Schedule & Pickup
+
+
+
+
+
+ {
+ if (!e || !dayjs(e).isValid()) {
+ setStartdate(dayjs().format('MM-DD-YYYY'));
+ return;
+ }
+ const diffDays = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd');
+ if (diffDays <= 0) {
+ setStartdate(dayjs(e).format('MM-DD-YYYY'));
+ setSelectedtime('');
+ setPickupSlot(null);
+ } else {
+ opentoast('Choose an upcoming date', 'warning');
+ setStartdate(dayjs().format('MM-DD-YYYY'));
+ }
+ }}
+ disablePast
+ slotProps={{
+ textField: {
+ size: 'small',
+ fullWidth: true,
+ InputLabelProps: { shrink: true },
+ InputProps: {
+ startAdornment: (
+
+
+
+ )
+ },
+ sx: {
+ '& .MuiOutlinedInput-root': {
+ borderRadius: '10px',
+ height: '36px',
+ paddingLeft: '10px'
+ }
+ }
+ }
+ }}
+ />
+
+
+
+ {
+ if (reason === 'clear' || !newValue) {
+ setSelectedtime(null);
+ setPickupSlot(null);
+ return;
+ }
+ if (!newValue.time) {
+ OpenToast('This slot has no time configured.', 'warning', 3000);
+ return;
+ }
+ const formattedTime = dayjs(newValue.time, 'HH:mm').format('hh:mm A');
+ setSelectedtime(formattedTime);
+ const finalDateTime = dayjs(
+ `${startdate} ${formattedTime}`,
+ 'MM-DD-YYYY hh:mm A'
+ ).format('YYYY-MM-DD hh:mm A');
+ setPickupSlot(finalDateTime);
+ }}
+ getOptionLabel={(option) =>
+ option ? `${option.name} (${dayjs(option.time, 'HH:mm').format('hh:mm A')})` : ''
+ }
+ renderInput={(params) => (
+
+
+ {params.InputProps.startAdornment}
+ >
+ )
+ }}
+ />
+ )}
+ />
+
+
+ {/* Pickup display (compact, single row) */}
+
+ {pickCust ? (
+
+
+
+
+
+
+ {pickCust.locationname || '—'}
+
+
+ {pickCust.address || '—'}
+
+
+
+ ) : (
+
+ Pickup auto-fills once a Business Location is selected.
+
+ )}
+
+
+
+
+
+
+ {/* Card: Notes */}
+
+
+ Order Notes
+ Applied to every order
+
+
+
+
+
+ Special Dispatch Notes
+
setOtherinstructions(e.target.value)}
+ sx={{
+ '& .MuiOutlinedInput-root': {
+ borderRadius: '10px',
+ padding: '0 10px',
+ alignItems: 'center',
+ fontSize: '12px',
+ background: '#ffffff',
+ height: '32px'
+ },
+ '& .MuiOutlinedInput-input': {
+ padding: '0 !important',
+ fontSize: '12px !important',
+ lineHeight: '32px'
+ }
}}
/>
-
- )}
- {/* ================================================= || Time || ================================================= */}
- {appId !== 0 && (
-
-
-
-
- {
- setStartdate(e);
- let dateres11 = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd');
- console.log('dateres11');
- console.log(dateres11);
- setSelectedtime('');
- if (dateres11 <= 0) {
- console.log('startdate', e);
- setStartdate(e);
+
+
- let arr = [];
- timeslotarr.map((val) => {
- if (dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0) {
- arr.push(val);
- }
- });
- } else {
- setAlertmessage('choose Upcoming Date');
- opentoast('choose Upcoming Date', 'warning');
- setStartdate(NaN);
- }
- }}
- value={dayjs(startdate)}
- sx={{ width: 'auto', mt: 0 }}
- disablePast
- />
-
-
- {/* {timeslotarr.length > 0 && (
-
-
-
-
-
-
- Time
-
-
-
-
-
- {
- setStartdate(e);
- let dateres11 = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd');
- console.log('dateres11');
- console.log(dateres11);
- setSelectedtime('');
- if (dateres11 <= 0) {
- console.log('startdate', e);
- setStartdate(e);
-
- let arr = [];
- timeslotarr.map((val) => {
- if (
- dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0
- ) {
- arr.push(val);
- }
- });
- } else {
- setAlertmessage('choose Upcoming Date');
- opentoast('choose Upcoming Date', 'warning');
- setStartdate(NaN);
- }
- }}
- value={dayjs(startdate)}
- sx={{ width: 'auto', mt: 0 }}
- disablePast
- />
-
-
-
-
-
-
- {timeslotarr.map((val, index) => {
- if (
- dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0
- ) {
- return (
-
-
- {
- console.log('selectedtime', val);
- setSelectedtime(val);
- }}
- // onClick={() => {
- // if (distance > appLocaRadius) {
- // setOpen(true);
- // } else if (showDistance) {
- // console.log('selectedtime', val);
- // setSelectedtime(val);
- // } else {
- // opentoast('Out of city limit', 'error');
- // }
- // }}
- />
-
-
- );
- }
- })}
-
-
-
-
-
- )} */}
-
-
- {
- if (reason === 'clear') {
- setSelectedtime(null);
- setPickupSlot(null);
- } else {
- // Convert to AM/PM and merge with date
- const formattedTime = dayjs(newValue.time, 'HH:mm').format('hh:mm A');
- setSelectedtime(formattedTime);
- const finalDateTime = dayjs(`${startdate} ${formattedTime}`, 'MM-DD-YYYY hh:mm A').format('YYYY-MM-DD hh:mm A');
- setPickupSlot(finalDateTime);
- }
- }}
- getOptionLabel={(option) => `${option.name} (${dayjs(option.time, 'HH:mm').format('hh:mm A')})`}
- renderInput={(params) => }
- />
-
-
- )}
-
-
-
- {/* ===================================================== || Drop || ===================================================== */}
-
-
-
-
- Drop ({dropCust?.length || 0})
+ {/* Card: Summary + Submit */}
+
+
+
+ Bulk Summary
-
- {/* ================= Upload CSV ================= */}
- {uploadType === 0 && (
- <>
- {fileName && (
-
-
-
- {fileName}
-
-
- )}
-
-
-
-
-
- {dropCust?.length > 0 ? 'Add More Files' : ' Upload CSV'}
-
-
- >
- )}
-
- {/* ================= Continue ================= */}
- {users.length >= 1 && uploadType === 0 && (
- {
- users.forEach((customer) => handleCheckboxChange1(customer));
- }}
- >
- Continue
-
- )}
-
- {/* ================= Select Customers ================= */}
- {uploadType === 1 && (
- {
- setIsCustomerOpen(true);
- setSearchCustList('');
- }}
- >
- Select Customers
-
- )}
-
- {/* ================= Upload Type ================= */}
-
-
- Upload Type
- {
- if (!appId || !tenantid || !locationid) {
- OpenToast('Please select Location, Tenant, Business!', 'warning', 3000);
- return;
- }
- setUploadType(Number(e.target.value));
- setDropCust([]);
- setUsers([]);
- setFileName('');
- }}
- >
- } label="Excel / CSV" />
- } label="Selection" />
-
-
-
-
-
- }
- >
-
-
- {dropCust?.length > 0 ? (
- <>
-
-
- S.No
- Customer
- Address
- Quantity
-
-
- Cash Collect
-
- Kms
-
- Charge
- Action
-
-
-
-
- {dropCust?.map((customer, index) => (
-
- {index + 1}
- {customer.firstname}
- {customer.address}
-
- {uploadType == 0 ? (
- {customer.quantity}
- ) : (
- handleQuantityChange(customer.customerid, e.target.value)}
- inputProps={{ min: 0 }}
- />
- )}
-
-
- {uploadType == 0 ? (
- ₹{Number(customer.collectionamt || 0).toFixed(2)}
- ) : (
- {
- if (e.target.value <= 0) {
- handleCollectionAmtChange(customer.customerid, 0);
- } else {
- handleCollectionAmtChange(customer.customerid, e.target.value);
- }
- }}
- inputProps={{ min: 0 }}
- InputProps={{
- startAdornment: ₹
- }}
- />
- )}
-
-
- {customer.distance}
- {`₹${customer?.totalcharge?.toFixed(2)}`}
-
- {
- <>
- handleCheckboxChange(event, customer)}
- onClick={() => handleCheckboxChange1(customer)}
- />
- >
- }
-
-
- ))}
- {dropCust?.length != 0 && (
-
- Total
-
-
-
- {`${totalQty} `}
-
-
-
- {`${totalCash?.toFixed(2)} `}
-
-
- {`${totaldist} `}
-
-
- {`₹${totalAmt?.toFixed(2)}`}
-
-
-
-
- )}
-
- >
- ) : (
-
+ Live totals
+
+
+ {(() => {
+ const metric = ({ icon: Icon, label, value, accent, active }) => (
+
- {/* Header */}
-
- {' '}
-
- Important Instructions
+
+
+
+
+
+ {label}
-
-
- {/* Ordered List */}
-
-
- Choose either Upload Type to upload CSV/Excel files, or
- Selection Type to select from saved customers.
+
+ {value}
+
+
+ );
+ return (
+
+
+ {metric({
+ icon: FaRoute,
+ label: 'Distance',
+ value: totaldist ? `${totaldist} km` : '—',
+ accent: '#1890ff',
+ active: !!totaldist
+ })}
+
+
+ {metric({
+ icon: FaBoxes,
+ label: 'Quantity',
+ value: totalQty || 0,
+ accent: '#16a34a',
+ active: !!totalQty
+ })}
+
+
+ {metric({
+ icon: FaMoneyBillWave,
+ label: 'Cash Collect',
+ value: `₹${Number(totalCash).toFixed(2)}`,
+ accent: '#d97706',
+ active: !!totalCash
+ })}
+
+
+ {metric({
+ icon: FaTruck,
+ label: 'Deliveries',
+ value: dropCust.length,
+ accent: '#65387a',
+ active: dropCust.length > 0
+ })}
+
+
+ );
+ })()}
-
- Uploaded CSV or Excel files must follow the required format and contain the correct column names.
-
+ {dropCust.length > 0 && (
+
+
+
+ ₹{Number(totalAmt).toFixed(2)}
+
+
+ )}
-
- Multiple files can be uploaded, but only one file at a time .
-
-
-
- Invalid or incorrectly formatted files will not be processed.
-
-
-
- )}
-
-
-
-
-
-
- {/* ================================================= || Notes || ================================================= */}
-
-
-
- setOtherinstructions(e.target.value)}
- />
-
-
+
{
- setLoading(true);
- setBtnLoading(true);
- createorders();
- }}
+ fullWidth
+ className="gradient-btn-create"
+ disabled={!canSubmit || btnLoading}
+ startIcon={!btnLoading && }
+ onClick={createorders}
+ sx={{ minHeight: '36px !important' }}
+ >
+ {btnLoading ? (
+
+ ) : (
+ `Dispatch ${dropCust.length || ''} ${dropCust.length === 1 ? 'Order' : 'Orders'}`.trim()
+ )}
+
+
+
+
+
+ {/* ============================== RIGHT 50% : File / Drop Preview ============================== */}
+
+
+ {/* Preview header (sticky inside card) */}
+
+
+
+ {previewMode === 'drops'
+ ? `Drop List`
+ : previewMode === 'preview'
+ ? `File Preview`
+ : `Preview`}
+
+
+
+ {previewMode === 'drops' && (
+
+ )}
+ {previewMode === 'preview' && (
+ }
+ sx={{
+ height: 22,
+ fontWeight: 700,
+ fontSize: 11,
+ bgcolor: 'rgba(245,158,11,0.12)',
+ color: '#b45309',
+ border: '1px solid rgba(245,158,11,0.30)',
+ '& .MuiChip-icon': { color: '#b45309' }
+ }}
+ />
+ )}
+ {previewMode !== 'empty' && (() => {
+ const prereqOk = appId && tenantid && locationid;
+ const handleHeaderPick = (val) => {
+ if (!prereqOk) {
+ OpenToast('Please select Location, Client, and Business Location first.', 'warning', 3000);
+ return;
+ }
+ setUploadType(val);
+ if (val === 0) {
+ document.getElementById('upload-file')?.click();
+ } else if (val === 1) {
+ setIsCustomerOpen(true);
+ setSearchCustList('');
+ }
+ };
+ return (
+
+ handleHeaderPick(0)}
+ startIcon={ }
+ sx={{
+ height: 24,
+ py: 0,
+ textTransform: 'none',
+ borderRadius: '6px',
+ fontSize: 10.5,
+ fontWeight: 600,
+ borderColor: '#cbd5e1',
+ color: '#475569',
+ '&:hover': { borderColor: '#1890ff', color: '#1890ff' }
+ }}
+ >
+ Excel / CSV
+
+ handleHeaderPick(1)}
+ startIcon={ }
+ sx={{
+ height: 24,
+ py: 0,
+ textTransform: 'none',
+ borderRadius: '6px',
+ fontSize: 10.5,
+ fontWeight: 600,
+ borderColor: '#cbd5e1',
+ color: '#475569',
+ '&:hover': { borderColor: '#65387a', color: '#65387a' }
+ }}
+ >
+ Selection
+
+
+ );
+ })()}
+
+
+ {fileName && (
+
+
+
+ {fileName}
+
+
+ )}
+
+ {previewMode === 'preview' && users.length >= 1 && (
+
- {btnLoading ? : 'Create'}
-
-
-
-
-
-
- {/* ============================================= || saved address Dialog || ============================================= */}
- {
- setIsCustomerOpen(false);
- }}
- fullWidth
- sx={{ minWidth: 'lg' }}
- >
-
-
- {`Select Drop Customers (${dropCust.length || 0})`}
-
-
-
+
+
+
+
+ Choose a Drop Source to begin
+
+
+ Select a source from below or the left panel to import or pick your delivery customers.
+
+
+
+ {tile({
+ value: 0,
+ icon: MdOutlineCloudUpload,
+ title: 'Excel / CSV',
+ sub: 'Bulk upload a sheet',
+ accent: '#1890ff'
+ })}
+ {tile({
+ value: 1,
+ icon: FaUsers,
+ title: 'Selection',
+ sub: 'Pick saved customers',
+ accent: '#65387a'
+ })}
+
+
+ );
+ })()}
+
+
+
+
+
+
+
+
+ {/* ============================== Saved customers dialog ============================== */}
+ setIsCustomerOpen(false)}
+ fullWidth
+ sx={{
+ '& .MuiDialog-paper': {
+ borderRadius: '16px',
+ overflow: 'hidden'
+ }
+ }}
+ >
+
+
+
+ {`Select Drop Customers (${dropCust.length || 0})`}
+
+
+
-
- {customerlist?.length == 0 ? (
-
-
+
+ {customerlist?.length === 0 ? (
+
+
) : (
-
- {customerlist &&
- customerlist?.map((customer, index) => (
-
+
+ {customerlist?.map((customer, index) => {
+ const checked = dropCust.some((c) => c.customerid === customer.customerid);
+ return (
+
cust.customerid === customer.customerid)} // Set the checked state of the checkbox based on whether the customer is in `dropCust`
+ checked={checked}
onChange={(event) => handleCheckboxChange(event, customer)}
/>
}
label={
-
-
- {`${customer.firstname} (${customer.contactno})`}
+
+
+ {customer.firstname} ({customer.contactno})
-
-
+
{customer.address}
-
+
}
/>
- ))}
+ );
+ })}
)}
-
+
setIsCustomerOpen(false)}
sx={{
- '&:hover': {
- bgcolor: dropCust.length == 0 ? 'red' : theme.palette.primary.main,
- color: 'white'
- }
- }}
- onClick={() => {
- setIsCustomerOpen(false);
+ borderRadius: '10px',
+ textTransform: 'none',
+ fontWeight: 600,
+ px: 3
}}
>
- {dropCust.length == 0 ? 'Close' : 'Continue'}
+ {dropCust.length === 0 ? 'Close' : 'Continue'}
diff --git a/src/pages/nearle/orders/orders.js b/src/pages/nearle/orders/orders.js
index d7ddc23..4fd0d08 100644
--- a/src/pages/nearle/orders/orders.js
+++ b/src/pages/nearle/orders/orders.js
@@ -142,6 +142,10 @@ const Orders = () => {
const [finaldeliveryList, setFinalDeliveryList] = useState([]);
const aiModeRef = useRef(0);
+ // Caches the inputs of the most recent AI Assign call so we can forward them
+ // to the /nearle/dispatch/preview page via navigate state (the page re-uses
+ // them for its Re-Assign button).
+ const aiMutationContextRef = useRef(null);
const rowsPerPage = 100;
const transportOptions = [
@@ -417,19 +421,25 @@ const Orders = () => {
setZoneData(data?.zones);
setMetaData(data?.meta);
setDispatchPreviewData(data);
- setAiDialog(true);
- // navigate('/nearle/orders/optimisedpreview', {
- // state: {
- // zoneSummary: data?.zone_analysis,
- // deliverylist: data?.details, // to deliveryDetails
- // zoneData: data?.zones,
- // metaData: data?.meta,
- // riderToken: rider.userfcmtoken,
- // appId,
- // aiMode: aiModeRef.current,
- // reassignOrders
- // }
- // });
+ // Route the AI Assign result to the dedicated Preview page (Dispatch
+ // view + Reconcile tab + Change Rider). The previous in-page dialog
+ // (aiDialog) is left intact but no longer opened — the page replaces
+ // it. All inputs needed for Re-Assign / Assign Orders are forwarded.
+ const ctx = aiMutationContextRef.current || {};
+ navigate('/nearle/dispatch/preview', {
+ state: {
+ dispatchPreviewData: data,
+ aiMode: ctx.aiMode ?? aiModeRef.current,
+ selectedMode: ctx.selectedMode || selectedMode,
+ deliveryData: ctx.deliveryData || [],
+ autoRiders: ctx.autoRiders || autoRiders || [],
+ absentRidersPayload: ctx.absentRidersPayload || [],
+ rider: ctx.rider || rider,
+ appId: ctx.appId ?? appId,
+ tenantId: ctx.tenantid ?? tenantid,
+ startdate
+ }
+ });
}
},
onError: (error) => {
@@ -562,6 +572,19 @@ const Orders = () => {
`Rider ${r.userid}`
}));
+ // Remember the inputs so the Preview page can re-run Re-Assign with the
+ // same payload without us having to re-derive it from current Orders state.
+ aiMutationContextRef.current = {
+ deliveryData,
+ absentRidersPayload,
+ autoRiders,
+ selectedMode,
+ aiMode: aiModeRef.current,
+ rider,
+ appId,
+ tenantid
+ };
+
if (aiModeRef.current == 0) {
// manual assign
createDeliveryMutation.mutate({
diff --git a/src/routes/MainRoutes.js b/src/routes/MainRoutes.js
index ed25929..276957b 100644
--- a/src/routes/MainRoutes.js
+++ b/src/routes/MainRoutes.js
@@ -49,6 +49,7 @@ const Riders = Loadable(lazy(() => import('pages/nearle/riders/riders')));
const Createrider = Loadable(lazy(() => import('pages/nearle/riders/createrider')));
const EditRider = Loadable(lazy(() => import('pages/nearle/riders/editRider')));
const Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch')));
+const DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview')));
// ==============================|| MAIN ROUTING ||============================== //
@@ -170,6 +171,10 @@ const MainRoutes = {
{
path: 'dispatch',
element:
+ },
+ {
+ path: 'dispatch/preview',
+ element:
}
]
},