updates on the changes
This commit is contained in:
@@ -163,6 +163,9 @@ export default function CreateOrder() {
|
||||
|
||||
const applyLocationToPickup = (loc) => {
|
||||
if (!loc) return;
|
||||
const lat = loc.latitude ?? loc.lat ?? loc.pickuplatitude ?? loc.locationlat;
|
||||
const lng = loc.longitude ?? loc.lng ?? loc.long ?? loc.pickuplongitude ?? loc.locationlong;
|
||||
|
||||
setPickCust((prev) => ({
|
||||
...prev,
|
||||
firstname: loc.contactname || loc.contactperson || prev.firstname || loc.locationname || 'Pickup Point',
|
||||
@@ -174,24 +177,9 @@ export default function CreateOrder() {
|
||||
suburb: loc.locationname || loc.city || '',
|
||||
city: loc.city || '',
|
||||
postcode: loc.pincode || loc.postcode || '',
|
||||
latitude: loc.latitude ? Number(loc.latitude) : '',
|
||||
longitude: loc.longitude ? Number(loc.longitude) : ''
|
||||
latitude: lat != null && lat !== '' ? Number(lat) : prev.latitude,
|
||||
longitude: lng != null && lng !== '' ? Number(lng) : prev.longitude
|
||||
}));
|
||||
|
||||
if (!loc.latitude || !loc.longitude) {
|
||||
const q = [loc.address, loc.city, loc.pincode].filter(Boolean).join(', ');
|
||||
if (q) {
|
||||
geocodeAddress(q).then((place) => {
|
||||
if (place?.geometry?.location) {
|
||||
setPickCust((prev) => ({
|
||||
...prev,
|
||||
latitude: place.geometry.location.lat(),
|
||||
longitude: place.geometry.location.lng()
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectLocation = (locId) => {
|
||||
@@ -207,8 +195,14 @@ export default function CreateOrder() {
|
||||
|
||||
// Tenant Pricing
|
||||
const clientRate = useMemo(() => {
|
||||
if (!selectedTenantId) return null;
|
||||
return (pricingList || []).find((p) => String(p.tenantid) === String(selectedTenantId));
|
||||
const list = Array.isArray(pricingList) ? pricingList : [];
|
||||
if (selectedTenantId) {
|
||||
const match = list.find((p) => String(p.tenantid) === String(selectedTenantId));
|
||||
if (match) return match;
|
||||
}
|
||||
// Fallback to default pricing rule or standard system rates
|
||||
const defaultRule = list.find((p) => p.isdefault || !p.tenantid || p.tenantid === 0);
|
||||
return defaultRule || list[0] || { baseprice: 40, priceperkm: 10, basedistance: 2 };
|
||||
}, [pricingList, selectedTenantId]);
|
||||
|
||||
// Route calculation
|
||||
@@ -227,7 +221,7 @@ export default function CreateOrder() {
|
||||
calculateDrivingDistance({ latitude: pLat, longitude: pLng }, { latitude: dLat, longitude: dLng })
|
||||
.then((distance) => {
|
||||
if (!active) return;
|
||||
setRoute({ distance, minutes: getLastRouteDurationMin(), resolved: true });
|
||||
setRoute({ distance: Math.max(1, distance || 1), minutes: getLastRouteDurationMin() || 15, resolved: true });
|
||||
})
|
||||
.catch(() => {
|
||||
if (active) setRoute({ distance: 0, minutes: null, resolved: false });
|
||||
@@ -240,13 +234,16 @@ export default function CreateOrder() {
|
||||
|
||||
// Total Charge calculation
|
||||
const totalCharge = useMemo(() => {
|
||||
if (!route.resolved || !clientRate) return 0;
|
||||
return calculateTotalCharge(
|
||||
route.distance,
|
||||
Number(clientRate.baseprice) || 0,
|
||||
Number(clientRate.priceperkm) || 0,
|
||||
Number(clientRate.basedistance) || 0
|
||||
);
|
||||
const base = Number(clientRate?.baseprice) || 40;
|
||||
const perKm = Number(clientRate?.priceperkm) || 10;
|
||||
const minKm = Number(clientRate?.basedistance) || 2;
|
||||
|
||||
if (!route.resolved || !route.distance) {
|
||||
return base;
|
||||
}
|
||||
|
||||
const calculated = calculateTotalCharge(route.distance, base, perKm, minKm);
|
||||
return Math.max(base, calculated);
|
||||
}, [route, clientRate]);
|
||||
|
||||
// Place selection helpers
|
||||
|
||||
@@ -138,13 +138,12 @@ export default function MultipleOrders() {
|
||||
|
||||
// Pricing synchronization
|
||||
useEffect(() => {
|
||||
if (!selectedTenantId || !pricingList?.length) return;
|
||||
const match = pricingList.find((p) => String(p.tenantid) === String(selectedTenantId));
|
||||
if (match) {
|
||||
setBasePrice(Number(match.baseprice) || 0);
|
||||
setPricePerKm(Number(match.priceperkm) || 0);
|
||||
setMinKm(Number(match.basedistance) || 0);
|
||||
}
|
||||
const list = Array.isArray(pricingList) ? pricingList : [];
|
||||
const match = list.find((p) => String(p.tenantid) === String(selectedTenantId));
|
||||
const rate = match || list.find((p) => p.isdefault || !p.tenantid || p.tenantid === 0) || list[0] || { baseprice: 40, priceperkm: 10, basedistance: 2 };
|
||||
setBasePrice(Number(rate.baseprice) || 40);
|
||||
setPricePerKm(Number(rate.priceperkm) || 10);
|
||||
setMinKm(Number(rate.basedistance) || 2);
|
||||
}, [selectedTenantId, pricingList]);
|
||||
|
||||
// Auto-select single location
|
||||
|
||||
@@ -2,11 +2,12 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
import dayjs from 'dayjs';
|
||||
import { Ban, FileSpreadsheet, MapPin, Package, Plus, Truck, Upload } from 'lucide-react';
|
||||
import { Button, ConfirmModal, DataTable, EmptyState, PageHeader, SearchInput, Stack, StatusBadge, Tabs, ZoneSelector } from '@/components/ds';
|
||||
import { useBulkCancelBookings, useCustomers } from '@/lib/doormileHooks';
|
||||
import { Ban, Bike, FileSpreadsheet, MapPin, Package, Plus, Sparkles, Truck, Upload, UserCheck } from 'lucide-react';
|
||||
import { Button, ConfirmModal, DataTable, EmptyState, Modal, PageHeader, SearchInput, Stack, StatusBadge, Tabs, ZoneSelector } from '@/components/ds';
|
||||
import { useBulkCancelBookings, useCustomers, useRiderOptions } from '@/lib/doormileHooks';
|
||||
import { useZone } from '@/lib/ZoneContext';
|
||||
import { createAutomationDeliveries } from '@/api/doormile';
|
||||
import { assignMilerToBooking, notifyMiler } from '@/api/doormile/endpoints';
|
||||
import { drainBookings } from '@/lib/assistant/scan';
|
||||
import { OpenToast, messageOf } from '@/api/doormile/notify';
|
||||
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
|
||||
@@ -56,7 +57,18 @@ const haversineKm = (lat1, lon1, lat2, lon2) => {
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
};
|
||||
|
||||
const chargeOf = (booking) => booking?.serviceoptions?.[0]?.estimatedprice;
|
||||
const chargeOf = (booking) => {
|
||||
const serviceOpt = booking?.serviceoptions?.[0];
|
||||
const val =
|
||||
serviceOpt?.estimatedprice ??
|
||||
serviceOpt?.price ??
|
||||
booking?.finalprice ??
|
||||
booking?.orderamount ??
|
||||
booking?.deliverycharges ??
|
||||
booking?.deliverycharge ??
|
||||
booking?.estimatedprice;
|
||||
return Number.isFinite(Number(val)) ? Number(val) : undefined;
|
||||
};
|
||||
|
||||
export default function Orders() {
|
||||
const navigate = useNavigate();
|
||||
@@ -72,6 +84,11 @@ export default function Orders() {
|
||||
const { selectedZone, selectedZoneId, isAllZones, matchesZone } = useZone();
|
||||
|
||||
const { data: customers = [] } = useCustomers();
|
||||
const { data: riders = [] } = useRiderOptions();
|
||||
|
||||
const [manualAssignOpen, setManualAssignOpen] = useState(false);
|
||||
const [selectedRiderId, setSelectedRiderId] = useState('');
|
||||
const [isAssigningManual, setIsAssigningManual] = useState(false);
|
||||
const bulkCancel = useBulkCancelBookings();
|
||||
|
||||
/* Polled: an operator watching the pending queue needs new bookings to
|
||||
@@ -213,6 +230,39 @@ export default function Orders() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleManualAssign = async () => {
|
||||
if (!selectedRiderId || !selectedIds.length) {
|
||||
OpenToast('Please select a rider first', 'warning');
|
||||
return;
|
||||
}
|
||||
const targetRider = riders.find((r) => String(r.userid || r.mileruserid || r.id) === String(selectedRiderId));
|
||||
if (!targetRider) {
|
||||
OpenToast('Please select a valid rider', 'warning');
|
||||
return;
|
||||
}
|
||||
const riderUserId = targetRider.userid || targetRider.mileruserid || targetRider.id;
|
||||
setIsAssigningManual(true);
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedIds.map((bookingId) =>
|
||||
assignMilerToBooking(bookingId, { mileruserid: riderUserId })
|
||||
)
|
||||
);
|
||||
if (targetRider.milerprofileid) {
|
||||
notifyMiler(targetRider.milerprofileid, 'DoormileXpress', `${selectedIds.length} orders assigned to you`).catch(() => {});
|
||||
}
|
||||
OpenToast(`${selectedIds.length} order${selectedIds.length === 1 ? '' : 's'} assigned to ${targetRider.displayname || targetRider.name || 'rider'}`, 'success');
|
||||
refetchOrders();
|
||||
setSelectedIds([]);
|
||||
setManualAssignOpen(false);
|
||||
setSelectedRiderId('');
|
||||
} catch (err) {
|
||||
OpenToast(messageOf(err, 'Failed to assign orders'), 'error', 4000);
|
||||
} finally {
|
||||
setIsAssigningManual(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -445,14 +495,91 @@ export default function Orders() {
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.18, ease: 'easeOut' }}
|
||||
className="fixed bottom-6 right-6 z-50 flex items-center gap-2 rounded-full border border-border bg-surface p-2 shadow-xl"
|
||||
className="fixed bottom-6 right-6 z-50 flex flex-wrap items-center gap-2.5 rounded-2xl border border-slate-200 bg-white/95 p-2.5 shadow-2xl backdrop-blur-md"
|
||||
>
|
||||
<Button onClick={assignOrders} loading={optimise.isPending}>
|
||||
<Truck className="mr-1.5 h-4 w-4" /> Assign orders ({selectedOrders.length})
|
||||
<div className="px-2 text-xs font-bold text-slate-700">
|
||||
{selectedOrders.length} selected
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-purple-600 hover:bg-purple-700 text-white shadow-xs"
|
||||
onClick={assignOrders}
|
||||
loading={optimise.isPending}
|
||||
>
|
||||
<Sparkles className="mr-1.5 h-4 w-4" /> AI Assign ({selectedOrders.length})
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-emerald-300 bg-emerald-50 text-emerald-800 hover:bg-emerald-100"
|
||||
onClick={() => {
|
||||
if (isAllZones) {
|
||||
OpenToast('Please select a specific Zone / Hub first', 'warning');
|
||||
return;
|
||||
}
|
||||
setManualAssignOpen(true);
|
||||
}}
|
||||
>
|
||||
<Bike className="mr-1.5 h-4 w-4 text-emerald-600" /> Manual Assign
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-red-600 hover:bg-red-50 hover:text-red-700"
|
||||
onClick={() => setCancelOpen(true)}
|
||||
>
|
||||
<Ban className="mr-1.5 h-4 w-4" /> Cancel
|
||||
</Button>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
open={manualAssignOpen}
|
||||
onOpenChange={setManualAssignOpen}
|
||||
title={`Manual Assign ${selectedIds.length} Order${selectedIds.length === 1 ? '' : 's'}`}
|
||||
description="Choose an active rider to assign the selected orders directly."
|
||||
>
|
||||
<div className="space-y-4 pt-2">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-800 mb-1.5">
|
||||
Select Rider <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
value={selectedRiderId}
|
||||
onChange={(e) => setSelectedRiderId(e.target.value)}
|
||||
className="w-full h-10 px-3 bg-white border border-slate-200 rounded-lg text-xs font-medium text-slate-900 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black"
|
||||
>
|
||||
<option value="">-- Choose an available rider --</option>
|
||||
{riders.map((r) => {
|
||||
const rId = String(r.userid || r.mileruserid || r.id);
|
||||
const rName = r.displayname || r.name || `Rider #${rId}`;
|
||||
const rPhone = r.mobile || r.phone ? ` · ${r.mobile || r.phone}` : '';
|
||||
const rStatus = r.status ? ` [${r.status}]` : '';
|
||||
return (
|
||||
<option key={rId} value={rId}>
|
||||
{rName} {rPhone} {rStatus}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 pt-3 border-t border-slate-100">
|
||||
<Button variant="outline" onClick={() => setManualAssignOpen(false)} disabled={isAssigningManual}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleManualAssign}
|
||||
disabled={!selectedRiderId || isAssigningManual}
|
||||
loading={isAssigningManual}
|
||||
>
|
||||
<UserCheck className="mr-1.5 h-4 w-4" /> Assign {selectedIds.length} Orders
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<ConfirmModal
|
||||
open={cancelOpen}
|
||||
onOpenChange={setCancelOpen}
|
||||
|
||||
Reference in New Issue
Block a user