updates on the autocomplete in the create order page

This commit is contained in:
2026-09-11 15:39:35 +05:30
parent 49ee0c5076
commit bc79ba4912
4 changed files with 719 additions and 240 deletions

View File

@@ -23,6 +23,8 @@ import dayjs from 'dayjs';
import AddressAutocomplete from '@/components/doormile/AddressAutocomplete';
import MapPinPickerModal from '@/components/doormile/MapPinPickerModal';
import { HubLocationSelect } from '@/components/doormile/HubLocationSelect';
import { MapContainer, Marker, Polyline, TileLayer, useMap } from 'react-leaflet';
import L from 'leaflet';
import {
createExpressBooking,
getAdminCustomers,
@@ -30,7 +32,8 @@ import {
getAdminTenants,
getTenantLocations
} from '@/api/doormile/endpoints';
import { calculateDrivingDistance, calculateTotalCharge, getLastRouteDurationMin } from '@/lib/distance';
import { calculateDrivingRoute, calculateTotalCharge, getLastRouteDurationMin } from '@/lib/distance';
import { saveRecentAddress } from '@/lib/geocodingService';
import { useHubs } from '@/lib/doormileHooks';
import {
PICKUP_SOURCE,
@@ -41,6 +44,32 @@ import {
} from '@/lib/orderFlow';
import { OpenToast } from '@/components/third-party/OpenToast';
const miniPickupIcon = new L.DivIcon({
className: 'mini-pickup-icon',
html: `<div style="background-color: #10b981; color: white; width: 22px; height: 22px; border-radius: 9999px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 10px; border: 2px solid white; box-shadow: 0 2px 5px rgba(0,0,0,0.3);">P</div>`,
iconSize: [22, 22],
iconAnchor: [11, 11]
});
const miniDropIcon = new L.DivIcon({
className: 'mini-drop-icon',
html: `<div style="background-color: #f97316; color: white; width: 22px; height: 22px; border-radius: 9999px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 10px; border: 2px solid white; box-shadow: 0 2px 5px rgba(0,0,0,0.3);">D</div>`,
iconSize: [22, 22],
iconAnchor: [11, 11]
});
function MiniRouteBoundsAdjuster({ bounds }) {
const map = useMap();
useEffect(() => {
if (bounds && bounds.length >= 2) {
try {
map.fitBounds(bounds, { padding: [20, 20], maxZoom: 15 });
} catch {}
}
}, [bounds, map]);
return null;
}
const SERVICE_OPTIONS = ['Normal', 'Fast', 'Superfast'];
const ITEM_CATEGORIES = ['General', 'Food & Beverage', 'Grocery', 'Pharmacy', 'Documents', 'Electronics'];
@@ -164,10 +193,17 @@ export default function CreateOrder() {
const { data: customerList = [], isLoading: isCustomersLoading } = useQuery({
queryKey: ['adminCustomers'],
queryFn: getAdminCustomers,
enabled: isCustomerModalOpen
queryFn: getAdminCustomers
});
const matchedCustomer = useMemo(() => {
const cleanPhone = String(dropCust.contactno || '').replace(/\D/g, '');
if (cleanPhone.length >= 8) {
return (customerList || []).find((c) => String(c.phone || '').replace(/\D/g, '').endsWith(cleanPhone));
}
return null;
}, [customerList, dropCust.contactno]);
const filteredCustomers = useMemo(() => {
const q = searchCustQuery.trim().toLowerCase();
if (!q) return customerList;
@@ -274,7 +310,7 @@ export default function CreateOrder() {
return defaultRule || list[0] || { baseprice: 40, priceperkm: 10, basedistance: 2 };
}, [pricingList, selectedTenantId]);
// Route calculation
// Route calculation with full polyline for mini-map preview
useEffect(() => {
const pLat = Number(pickCust.latitude);
const pLng = Number(pickCust.longitude);
@@ -282,18 +318,23 @@ export default function CreateOrder() {
const dLng = Number(dropCust.longitude);
if (!pLat || !pLng || !dLat || !dLng) {
setRoute({ distance: 0, minutes: null, resolved: false });
setRoute({ distance: 0, minutes: null, polyline: [], resolved: false });
return;
}
let active = true;
calculateDrivingDistance({ latitude: pLat, longitude: pLng }, { latitude: dLat, longitude: dLng })
.then((distance) => {
calculateDrivingRoute({ latitude: pLat, longitude: pLng }, { latitude: dLat, longitude: dLng })
.then((res) => {
if (!active) return;
setRoute({ distance: Math.max(1, distance || 1), minutes: getLastRouteDurationMin() || 15, resolved: true });
setRoute({
distance: Math.max(1, res.distance || 1),
minutes: res.minutes || 15,
polyline: res.polyline || [[pLat, pLng], [dLat, dLng]],
resolved: true
});
})
.catch(() => {
if (active) setRoute({ distance: 0, minutes: null, resolved: false });
if (active) setRoute({ distance: 0, minutes: null, polyline: [], resolved: false });
});
return () => {
@@ -354,6 +395,25 @@ export default function CreateOrder() {
return Math.max(base, calculated);
}, [route, clientRate]);
// Active City & Hub Bias for smart address autocomplete
const activeCityBias = useMemo(() => {
if (selectedLocation?.latitude && selectedLocation?.longitude) {
return {
lat: Number(selectedLocation.latitude),
lng: Number(selectedLocation.longitude),
city: selectedLocation.city || selectedLocation.locationname || ''
};
}
if (pickCust?.latitude && pickCust?.longitude) {
return {
lat: Number(pickCust.latitude),
lng: Number(pickCust.longitude),
city: pickCust.city || ''
};
}
return undefined;
}, [selectedLocation, pickCust.latitude, pickCust.longitude, pickCust.city]);
// Place selection helpers
const handlePlaceSelect = (partyType, place) => {
if (!place) return;
@@ -519,6 +579,15 @@ export default function CreateOrder() {
try {
const res = await createExpressBooking(payload);
if (res?.success) {
saveRecentAddress({
formatted_address: dropCust.address,
name: dropCust.firstname,
suburb: dropCust.suburb,
city: dropCust.city,
postcode: dropCust.postcode,
latitude: Number(dropCust.latitude),
longitude: Number(dropCust.longitude)
});
queryClient.invalidateQueries({ queryKey: ['doormile', 'bookings'] });
queryClient.invalidateQueries({ queryKey: ['ordersBulk'] });
OpenToast('Order created successfully', 'success', 2500);
@@ -776,11 +845,15 @@ export default function CreateOrder() {
id="pickup-address"
label="Pickup Address"
required
placeholder="Search address, building or locality"
placeholder="Search area, street, society, pincode or paste link…"
value={pickCust.address}
bias={activeCityBias}
onChange={(text) => setPickCust((prev) => ({ ...prev, address: text }))}
onPlaceSelected={(place) => handlePlaceSelect('pickup', place)}
onOpenMapPin={() => setMapPickerTarget('pickup')}
onLandmarkExtracted={(lmk) => {
setPickCust((prev) => (!prev.landmark ? { ...prev, landmark: lmk } : prev));
}}
/>
</div>
@@ -851,6 +924,26 @@ export default function CreateOrder() {
</button>
</div>
{/* Smart Customer Auto-Fill Banner */}
{matchedCustomer && (!dropCust.firstname || dropCust.firstname !== matchedCustomer.name) && (
<div className="p-2.5 bg-blue-50/90 border border-blue-200 rounded-xl flex items-center justify-between gap-3 animate-in fade-in duration-200">
<div className="flex items-center gap-2 text-xs text-blue-900">
<span className="font-bold text-blue-700">✨ Existing Contact Found:</span>
<span className="font-semibold">{matchedCustomer.name}</span>
{matchedCustomer.phone && (
<span className="font-mono text-blue-600 text-[11px]">({matchedCustomer.phone})</span>
)}
</div>
<button
type="button"
onClick={() => handlePickCustomer(matchedCustomer)}
className="h-7 px-3 bg-blue-600 hover:bg-blue-700 text-white text-xs font-semibold rounded-lg transition-colors cursor-pointer shrink-0 shadow-2xs"
>
Auto-Fill
</button>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-slate-800 mb-1">
@@ -887,12 +980,15 @@ export default function CreateOrder() {
id="drop-address"
label="Delivery Address"
required
placeholder="Search delivery address, society or landmark"
placeholder="Search area, street, society, pincode or paste link…"
value={dropCust.address}
bias={pickCust.latitude ? { lat: Number(pickCust.latitude), lng: Number(pickCust.longitude) } : undefined}
bias={activeCityBias}
onChange={(text) => setDropCust((prev) => ({ ...prev, address: text }))}
onPlaceSelected={(place) => handlePlaceSelect('drop', place)}
onOpenMapPin={() => setMapPickerTarget('drop')}
onLandmarkExtracted={(lmk) => {
setDropCust((prev) => (!prev.landmark ? { ...prev, landmark: lmk } : prev));
}}
/>
</div>
@@ -1246,6 +1342,52 @@ export default function CreateOrder() {
</div>
</div>
{/* Mini Interactive Route Map Preview */}
{route.resolved && Number(pickCust.latitude) && Number(dropCust.latitude) && (
<div className="rounded-xl overflow-hidden border border-slate-200 shadow-2xs mt-2 relative">
<div className="h-36 w-full relative z-0">
<MapContainer
center={[
(Number(pickCust.latitude) + Number(dropCust.latitude)) / 2,
(Number(pickCust.longitude) + Number(dropCust.longitude)) / 2
]}
zoom={12}
scrollWheelZoom={false}
dragging={false}
zoomControl={false}
attributionControl={false}
className="h-full w-full"
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Marker
position={[Number(pickCust.latitude), Number(pickCust.longitude)]}
icon={miniPickupIcon}
/>
<Marker
position={[Number(dropCust.latitude), Number(dropCust.longitude)]}
icon={miniDropIcon}
/>
{route.polyline && route.polyline.length > 0 && (
<Polyline positions={route.polyline} color="#2563eb" weight={4} opacity={0.85} />
)}
<MiniRouteBoundsAdjuster
bounds={[
[Number(pickCust.latitude), Number(pickCust.longitude)],
[Number(dropCust.latitude), Number(dropCust.longitude)]
]}
/>
</MapContainer>
</div>
<div className="px-3 py-1.5 bg-slate-50 border-t border-slate-100 flex items-center justify-between text-[10px] text-slate-500 font-medium">
<span className="flex items-center gap-1.5">
<span className="w-2 h-2 rounded-full bg-emerald-500" />
Pickup ➔ <span className="w-2 h-2 rounded-full bg-orange-500" /> Delivery
</span>
<span className="font-semibold text-slate-700">{route.distance} km road path</span>
</div>
</div>
)}
{/* Shown only when it exists, and labelled as unbilled so nobody
reconciles the invoice against the wrong number. */}
{isCustomerPickup && deadhead.resolved && (