mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
feat(bookings): refactor location handling for first/last mile and improve geocoding logic
This commit is contained in:
@@ -50,15 +50,19 @@ const MIN_QUERY_LEN = 2;
|
||||
// first (Nominatim still returns global matches if nothing local fits).
|
||||
const SEARCH_COUNTRYCODES = "et,dj";
|
||||
|
||||
/** Forward-geocode a free-text query to candidate places (free Nominatim API). */
|
||||
async function searchPlaces(query: string, signal: AbortSignal): Promise<GeocodeResult[]> {
|
||||
/** One Nominatim forward-geocode request. `countryCodes` biases to a region. */
|
||||
async function nominatimSearch(
|
||||
query: string,
|
||||
signal: AbortSignal,
|
||||
countryCodes?: string,
|
||||
): Promise<GeocodeResult[]> {
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
format: "jsonv2",
|
||||
addressdetails: "0",
|
||||
limit: "8",
|
||||
countrycodes: SEARCH_COUNTRYCODES,
|
||||
});
|
||||
if (countryCodes) params.set("countrycodes", countryCodes);
|
||||
const res = await fetch(`${NOMINATIM_URL}?${params}`, {
|
||||
signal,
|
||||
headers: { Accept: "application/json", "Accept-Language": "en" },
|
||||
@@ -76,6 +80,17 @@ async function searchPlaces(query: string, signal: AbortSignal): Promise<Geocode
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward-geocode a free-text query. We try the EDR corridor (ET/DJ) first so
|
||||
* local addresses rank highest, then fall back to a global search when nothing
|
||||
* local matches — so the field never looks "broken" for an out-of-region query.
|
||||
*/
|
||||
async function searchPlaces(query: string, signal: AbortSignal): Promise<GeocodeResult[]> {
|
||||
const local = await nominatimSearch(query, signal, SEARCH_COUNTRYCODES);
|
||||
if (local.length > 0) return local;
|
||||
return nominatimSearch(query, signal);
|
||||
}
|
||||
|
||||
/** Reverse-geocode a dropped pin to its nearest address. */
|
||||
async function reverseGeocode(lat: number, lng: number): Promise<string> {
|
||||
const params = new URLSearchParams({
|
||||
@@ -95,6 +110,21 @@ async function reverseGeocode(lat: number, lng: number): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Leaflet computes its tile layout from the container size at mount. When the
|
||||
* map is revealed inside a just-toggled section it can mount before layout
|
||||
* settles and render grey tiles — invalidating the size on the next frame
|
||||
* forces a correct redraw.
|
||||
*/
|
||||
function InvalidateSizeOnMount() {
|
||||
const map = useMap();
|
||||
useEffect(() => {
|
||||
const id = setTimeout(() => map.invalidateSize(), 0);
|
||||
return () => clearTimeout(id);
|
||||
}, [map]);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Recenters the map imperatively when the pinned coordinate changes. */
|
||||
function MapRecenter({ lat, lng }: { lat: number | null; lng: number | null }) {
|
||||
const map = useMap();
|
||||
@@ -158,22 +188,28 @@ export function LocationPicker({
|
||||
}
|
||||
setSearching(true);
|
||||
combobox.openDropdown();
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
const handle = setTimeout(async () => {
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
try {
|
||||
const found = await searchPlaces(q, controller.signal);
|
||||
if (controller.signal.aborted) return;
|
||||
setResults(found);
|
||||
combobox.openDropdown();
|
||||
} catch (err) {
|
||||
// Ignore aborts (a newer keystroke superseded this request).
|
||||
if ((err as Error)?.name !== "AbortError") setResults([]);
|
||||
} finally {
|
||||
setSearching(false);
|
||||
if (!controller.signal.aborted) setSearching(false);
|
||||
}
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
return () => clearTimeout(handle);
|
||||
// Cancel both the pending debounce AND any in-flight request when the query
|
||||
// changes, so a stale response can't overwrite newer results.
|
||||
return () => {
|
||||
clearTimeout(handle);
|
||||
controller.abort();
|
||||
};
|
||||
}, [query, combobox]);
|
||||
|
||||
const selectResult = useCallback(
|
||||
@@ -277,6 +313,7 @@ export function LocationPicker({
|
||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
<InvalidateSizeOnMount />
|
||||
<ClickToPin onPick={handlePin} />
|
||||
<MapRecenter lat={value.lat} lng={value.lng} />
|
||||
{hasPin && (
|
||||
|
||||
@@ -334,6 +334,10 @@ export const stepFields: Record<number, Array<Path<BookingFormValues>>> = {
|
||||
"equipmentReturn",
|
||||
"customsClearingEnabled",
|
||||
"customsClearingAgent",
|
||||
// First/last-mile pickup & delivery locations are captured inline in the
|
||||
// service step, right under each trucking toggle.
|
||||
"firstMile",
|
||||
"lastMile",
|
||||
],
|
||||
3: ["cargoType", "cargoWeight", "cargoTypePath", "containers"],
|
||||
4: [
|
||||
@@ -341,9 +345,6 @@ export const stepFields: Record<number, Array<Path<BookingFormValues>>> = {
|
||||
"destinationYard",
|
||||
"primaryRouteQuantity",
|
||||
"extraRoutes",
|
||||
// First/last-mile pickup & delivery locations are captured here on the map.
|
||||
"firstMile",
|
||||
"lastMile",
|
||||
"isHazardous",
|
||||
"isRefrigerated",
|
||||
],
|
||||
|
||||
@@ -124,7 +124,7 @@ export function Step2ServiceType({
|
||||
onChange={(value) => {
|
||||
field.onChange(value);
|
||||
if (!value) {
|
||||
// Clear the captured pick-up location (set on the Route step).
|
||||
// Toggling off clears the captured pick-up location below.
|
||||
form.setValue(
|
||||
"firstMile",
|
||||
{ enabled: false, pickUpAddress: "", lat: null, lng: null },
|
||||
@@ -187,7 +187,7 @@ export function Step2ServiceType({
|
||||
onChange={(value) => {
|
||||
field.onChange(value);
|
||||
if (!value) {
|
||||
// Clear the captured delivery location (set on the Route step).
|
||||
// Toggling off clears the captured delivery location below.
|
||||
form.setValue(
|
||||
"lastMile",
|
||||
{ enabled: false, deliveryAddress: "", lat: null, lng: null },
|
||||
|
||||
Reference in New Issue
Block a user