updates on the minor changes like zone updates and datepicker fix
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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 }))}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user