updates on the minor changes like zone updates and datepicker fix

This commit is contained in:
2026-09-28 20:13:40 +05:30
parent 05a4cbdf8a
commit 92e8da2842
5 changed files with 847 additions and 250 deletions

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { useEffect, useRef, useState } from 'react';
import { Clock, Compass, Crosshair, Loader2, MapPin, Navigation, X } from 'lucide-react';
import {
cityNamedIn,
extractLandmark,
getAddressSuggestions,
geocodeAddress as serviceGeocodeAddress,
@@ -38,9 +39,17 @@ const AddressAutocomplete = ({
value,
onChange,
onPlaceSelected,
// Fired when the text stops describing the place that was picked — cleared,
// or rewritten to another area. The page should drop that place's pin: it
// used to stay, so the form saved one address with another address's pin.
onPlaceCleared,
onOpenMapPin,
onLandmarkExtracted,
bias,
// A city scope from buildCityScope(): suggestions only from that city. The
// operator can widen one search to all cities from the list's footer, for
// an intercity drop.
scope,
fullWidth = true,
disabled = false,
className = '',
@@ -60,21 +69,100 @@ const AddressAutocomplete = ({
const wrapRef = useRef(null);
const activeRef = useRef(0);
const skipNextSearchRef = useRef(false);
// True only while the text is the operator's typing. Text the page sets (a
// hub location filling the pickup, a chosen customer's address) or a picked
// suggestion is not searched, not even when the hub's city scope changes
// underneath it: that re-search opened an unasked-for list of
// "124 Gandhipuram Main Rd" matches on every Create Order load.
const typedRef = useRef(false);
// The in-flight search, aborted when a newer one starts. Ignoring stale
// answers was not enough: the requests still ran against the free services'
// allowance and slowed the one that mattered.
const abortRef = useRef(null);
// Name of the picked place. Typing that keeps it ("12/A, " + it) keeps the
// pin; text that no longer contains it means the pin no longer applies.
const pickedNameRef = useRef('');
const editText = (text) => {
typedRef.current = true;
setInputValue(text);
onChange?.(text);
const picked = pickedNameRef.current;
if (picked && !text.toLowerCase().includes(picked)) {
pickedNameRef.current = '';
onPlaceCleared?.();
}
};
useEffect(() => () => abortRef.current?.abort(), []);
const listId = `${id || 'address'}-suggestions`;
const optionId = (index) => `${listId}-option-${index}`;
useEffect(() => {
if (value !== undefined && value !== inputValue) {
typedRef.current = false;
setInputValue(value || '');
}
}, [value]);
const biasKey = `${bias?.lat ?? ''},${bias?.lng ?? ''},${bias?.city ?? ''}`;
// "Search all cities" for this field. Reset when the scope itself changes —
// choosing another hub location means searching that city again.
const [scopeOff, setScopeOff] = useState(false);
const scopeCity = scope?.city || '';
useEffect(() => setScopeOff(false), [scopeCity]);
const activeScope = scope && !scopeOff ? scope : undefined;
const biasKey = `${bias?.lat ?? ''},${bias?.lng ?? ''},${bias?.city ?? ''}|${activeScope ? activeScope.city : 'all'}`;
// A city named at the end of the text ("gandhi nagar coimbatore") is searched
// in that city whatever the hub; the footer says so rather than naming the hub.
const typedCity = cityNamedIn(inputValue);
const scopeFooter = scope ? (
<div className="px-3 py-1.5 bg-slate-50 border-t border-slate-100 flex items-center justify-between gap-2 text-[11px] text-slate-600">
<span className="flex items-center gap-1.5 min-w-0">
<MapPin className="w-3 h-3 text-slate-500 shrink-0" />
<span className="truncate">
{typedCity && typedCity !== scope.city
? `Showing ${typedCity}, from your text`
: activeScope
? `Showing ${scope.city || 'this hub’s city'} only`
: 'Showing all cities'}
</span>
</span>
<button
type="button"
// mousedown, so the input keeps focus and the list stays open.
onMouseDown={(e) => {
e.preventDefault();
setScopeOff((off) => !off);
}}
className="font-semibold text-blue-600 hover:underline cursor-pointer shrink-0"
>
{activeScope ? 'Search all cities' : `Only ${scope.city || 'this city'}`}
</button>
</div>
) : null;
const fetchPredictions = useDebouncedCallback((query) => {
const seq = ++activeRef.current;
getAddressSuggestions(query, { bias, limit: 12 })
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
getAddressSuggestions(query, {
bias,
scope: activeScope,
limit: 12,
signal: controller.signal,
// Recents, Nominatim and India Post answer in ~1-2s; Photon can take 8s
// when throttled. Show the fast ones now; the spinner stays until the rest.
onPartial: (partial) => {
if (seq !== activeRef.current) return;
setOptions(partial);
setIsRecentMode(false);
setNoResults(false);
setIsOpen(true);
}
})
.then((results) => {
if (seq !== activeRef.current) return;
setLoading(false);
@@ -109,13 +197,11 @@ const AddressAutocomplete = ({
// Trigger search when input changes
useEffect(() => {
if (skipNextSearchRef.current) {
skipNextSearchRef.current = false;
return;
}
if (!typedRef.current) return;
setNoResults(false);
if (!inputValue || inputValue.trim().length < 2) {
activeRef.current++;
abortRef.current?.abort();
const recents = getRecentAddresses(5);
if (recents.length > 0 && (!inputValue || inputValue.length === 0)) {
setOptions(recents);
@@ -128,6 +214,12 @@ const AddressAutocomplete = ({
return;
}
// Drop the previous search's list. It stayed on screen, clickable, while
// the new search ran: typing a Coimbatore address after a Bengaluru one
// showed Bengaluru results under "Showing Coimbatore only".
setOptions([]);
setIsRecentMode(false);
setIsOpen(true);
setLoading(true);
fetchPredictions(inputValue);
}, [inputValue, biasKey]);
@@ -148,8 +240,10 @@ const AddressAutocomplete = ({
const pick = (option) => {
const formatted = option.formatted_address || option.name || '';
skipNextSearchRef.current = true;
typedRef.current = false;
activeRef.current++;
abortRef.current?.abort();
pickedNameRef.current = String(option.name || formatted.split(',')[0] || '').trim().toLowerCase();
setInputValue(formatted);
saveRecentAddress(option);
onChange?.(formatted);
@@ -212,9 +306,11 @@ const AddressAutocomplete = ({
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setActiveOption((current) => (current <= 0 ? options.length - 1 : current - 1));
} else if (e.key === 'Enter' && activeOption >= 0) {
} else if (e.key === 'Enter') {
// Enter with nothing highlighted takes the top suggestion, so the field
// can be filled without leaving the keyboard.
e.preventDefault();
pick(options[activeOption]);
pick(options[activeOption >= 0 ? activeOption : 0]);
}
};
@@ -269,10 +365,7 @@ const AddressAutocomplete = ({
aria-autocomplete="list"
aria-activedescendant={activeOption >= 0 ? optionId(activeOption) : undefined}
onFocus={handleFocus}
onChange={(e) => {
setInputValue(e.target.value);
onChange?.(e.target.value);
}}
onChange={(e) => editText(e.target.value)}
onKeyDown={(e) => {
onKeyDown(e);
if (e.key === 'Escape') setIsOpen(false);
@@ -290,8 +383,11 @@ const AddressAutocomplete = ({
<button
type="button"
onClick={() => {
setInputValue('');
onChange?.('');
// Cleared text is always a cleared place, picked here or filled
// in by the page (a chosen customer's saved address).
const hadPick = Boolean(pickedNameRef.current);
editText('');
if (!hadPick) onPlaceCleared?.();
setOptions([]);
setIsOpen(false);
}}
@@ -315,13 +411,37 @@ const AddressAutocomplete = ({
</div>
</div>
{isOpen && loading && options.length === 0 && (
<div
role="status"
className="absolute z-[9999] left-0 right-0 mt-1.5 bg-white border border-slate-200 rounded-xl shadow-2xl px-3.5 py-2.5 text-[11px] text-slate-500 flex items-center gap-2"
>
<Loader2 className="w-3.5 h-3.5 animate-spin" />
<span>Searching{activeScope?.city ? ` in ${activeScope.city}` : ''}…</span>
</div>
)}
{isOpen && noResults && options.length === 0 && !loading && (
<div
id={listId}
role="status"
className="absolute z-[9999] left-0 right-0 mt-1.5 bg-white border border-slate-200 rounded-xl shadow-2xl px-3.5 py-3 text-[11px] text-slate-600"
>
<div className="font-semibold text-slate-800">No matching address found</div>
<div className="font-semibold text-slate-800">
{activeScope ? `No matching address in ${activeScope.city || 'this city'}` : 'No matching address found'}
</div>
{activeScope && (
<button
type="button"
onMouseDown={(e) => {
e.preventDefault();
setScopeOff(true);
}}
className="mt-1 font-semibold text-blue-600 hover:underline cursor-pointer"
>
Search all cities
</button>
)}
<div className="mt-0.5">
Try just the area name or the 6-digit pincode
{onOpenMapPin ? (
@@ -429,6 +549,7 @@ const AddressAutocomplete = ({
);
})}
</ul>
{!isRecentMode && scopeFooter}
</div>
)}
</div>
@@ -442,9 +563,11 @@ AddressAutocomplete.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func,
onPlaceSelected: PropTypes.func,
onPlaceCleared: PropTypes.func,
onOpenMapPin: PropTypes.func,
onLandmarkExtracted: PropTypes.func,
bias: PropTypes.object,
scope: PropTypes.object,
fullWidth: PropTypes.bool,
disabled: PropTypes.bool,
className: PropTypes.string,

View File

@@ -89,11 +89,15 @@ export default function MapPinPickerModal({
initialLat,
initialLng,
initialAddress = '',
title = 'Pin Exact Location on Map'
title = 'Pin Exact Location on Map',
// Where to open when there is no pin yet: the hub's city. It always opened
// on Bengaluru, so a Coimbatore operator started every pin 350 km away.
defaultCenter
}) {
// Default coordinates (Bengaluru / Central fallback if none provided)
const defaultLat = Number(initialLat) || 12.9716;
const defaultLng = Number(initialLng) || 77.5946;
const fallbackLat = Number(defaultCenter?.lat) || 12.9716;
const fallbackLng = Number(defaultCenter?.lng) || 77.5946;
const defaultLat = Number(initialLat) || fallbackLat;
const defaultLng = Number(initialLng) || fallbackLng;
const [coords, setCoords] = useState({ lat: defaultLat, lng: defaultLng });
const [mapCenter, setMapCenter] = useState([defaultLat, defaultLng]);
@@ -109,8 +113,8 @@ export default function MapPinPickerModal({
useEffect(() => {
if (isOpen) {
const lat = Number(initialLat) || 12.9716;
const lng = Number(initialLng) || 77.5946;
const lat = Number(initialLat) || fallbackLat;
const lng = Number(initialLng) || fallbackLng;
setCoords({ lat, lng });
setMapCenter([lat, lng]);
setZoomLevel(initialLat && initialLng ? 16 : 13);
@@ -346,5 +350,6 @@ MapPinPickerModal.propTypes = {
initialLat: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
initialLng: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
initialAddress: PropTypes.string,
defaultCenter: PropTypes.shape({ lat: PropTypes.number, lng: PropTypes.number }),
title: PropTypes.string
};

File diff suppressed because it is too large Load Diff

View File

@@ -304,6 +304,7 @@ export default function CreateCustomer() {
bias={bias}
onChange={(text) => set('address')(text)}
onPlaceSelected={applyPlace}
onPlaceCleared={() => setForm((current) => ({ ...current, latitude: '', longitude: '', suburb: '', postcode: '' }))}
onOpenMapPin={() => setIsMapOpen(true)}
onLandmarkExtracted={(lmk) => setForm((current) => (current.landmark ? current : { ...current, landmark: lmk }))}
/>

View File

@@ -57,7 +57,7 @@ import {
calculateDrivingRoute,
calculateTotalCharge
} from '@/lib/distance';
import { geocodeAddress, saveRecentAddress } from '@/lib/geocodingService';
import { buildCityScope, geocodeAddress, saveRecentAddress } from '@/lib/geocodingService';
import { useHubs } from '@/lib/doormileHooks';
import {
PICKUP_SOURCE,
@@ -618,13 +618,33 @@ export default function CreateOrder() {
return Math.max(base, calculated);
}, [route, clientRate]);
// The selected hub location's city. Address suggestions come only from it:
// a Coimbatore location searches Coimbatore, a Bengaluru or Hyderabad one
// searches that city. It used to be a bias alone, which ranked the hub's
// city first and still filled the list with Bengaluru and Chennai. An
// intercity drop widens its own search from the suggestion list's footer.
const cityScope = useMemo(
() =>
selectedLocation
? buildCityScope({
latitude: selectedLocation.latitude ?? selectedLocation.lat,
longitude: selectedLocation.longitude ?? selectedLocation.lng,
city: selectedLocation.city,
pincode: selectedLocation.pincode || selectedLocation.postcode
})
: null,
[selectedLocation]
);
// 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 || ''
// Not the location's name: "RS Puram Kitchen" is not a city, and the
// ranking compares this against each suggestion's city.
city: selectedLocation.city || cityScope?.city || ''
};
}
if (pickCust?.latitude && pickCust?.longitude) {
@@ -635,7 +655,7 @@ export default function CreateOrder() {
};
}
return undefined;
}, [selectedLocation, pickCust.latitude, pickCust.longitude, pickCust.city]);
}, [selectedLocation, cityScope, pickCust.latitude, pickCust.longitude, pickCust.city]);
// Place selection helpers
const handlePlaceSelect = (partyType, place) => {
@@ -1141,8 +1161,10 @@ export default function CreateOrder() {
placeholder="Search area, street, society, pincode or paste link…"
value={pickCust.address}
bias={activeCityBias}
scope={cityScope}
onChange={(text) => setPickCust((prev) => ({ ...prev, address: text }))}
onPlaceSelected={(place) => handlePlaceSelect('pickup', place)}
onPlaceCleared={() => setPickCust((prev) => ({ ...prev, latitude: '', longitude: '', suburb: '', postcode: '' }))}
onOpenMapPin={() => setMapPickerTarget('pickup')}
onLandmarkExtracted={(lmk) => {
setPickCust((prev) => (!prev.landmark ? { ...prev, landmark: lmk } : prev));
@@ -1177,7 +1199,8 @@ export default function CreateOrder() {
type="text"
placeholder="Pincode"
value={pickCust.postcode}
onChange={(e) => setPickCust({ ...pickCust, postcode: e.target.value })}
onChange={(e) => setPickCust({ ...pickCust, postcode: e.target.value.replace(/\D/g, '').slice(0, 6) })}
inputMode="numeric"
className="w-full h-9 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 transition-colors font-mono"
/>
</div>
@@ -1276,8 +1299,10 @@ export default function CreateOrder() {
placeholder="Search area, street, society, pincode or paste link…"
value={dropCust.address}
bias={activeCityBias}
scope={cityScope}
onChange={(text) => setDropCust((prev) => ({ ...prev, address: text }))}
onPlaceSelected={(place) => handlePlaceSelect('drop', place)}
onPlaceCleared={() => setDropCust((prev) => ({ ...prev, latitude: '', longitude: '', suburb: '', postcode: '' }))}
onOpenMapPin={() => setMapPickerTarget('drop')}
onLandmarkExtracted={(lmk) => {
setDropCust((prev) => (!prev.landmark ? { ...prev, landmark: lmk } : prev));
@@ -1312,7 +1337,8 @@ export default function CreateOrder() {
type="text"
placeholder="Pincode"
value={dropCust.postcode}
onChange={(e) => setDropCust({ ...dropCust, postcode: e.target.value })}
onChange={(e) => setDropCust({ ...dropCust, postcode: e.target.value.replace(/\D/g, '').slice(0, 6) })}
inputMode="numeric"
className="w-full h-9 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 transition-colors font-mono"
/>
</div>
@@ -1934,6 +1960,7 @@ export default function CreateOrder() {
initialAddress={mapPickerTarget === 'pickup' ? pickCust.address : dropCust.address}
initialLat={mapPickerTarget === 'pickup' ? pickCust.latitude : dropCust.latitude}
initialLng={mapPickerTarget === 'pickup' ? pickCust.longitude : dropCust.longitude}
defaultCenter={cityScope ? { lat: cityScope.lat, lng: cityScope.lng } : undefined}
onConfirm={(place) => {
if (mapPickerTarget) {
handlePlaceSelect(mapPickerTarget, place);