updates on the build regarding the create order dropdown and fix on the customer and createorder page

This commit is contained in:
2026-08-05 13:24:05 +05:30
parent 8a8bc4ec2f
commit 309157678d
6 changed files with 206 additions and 21 deletions

View File

@@ -2,12 +2,14 @@ import React, { useEffect, useMemo, useState } from 'react';
import { Autocomplete, TextField, CircularProgress } from '@mui/material';
import { debounce } from '@mui/material/utils';
import { OpenLocationCode } from 'open-location-code';
// Free OSM Nominatim geocoder — replaces Google Places Autocomplete /
// react-geocode across the console. No API key required. Nominatim's usage
// policy caps the public endpoint at ~1 req/sec, so every caller here is
// debounced (500ms) and cancels stale in-flight predictions.
const NOMINATIM_SEARCH_URL = 'https://nominatim.openstreetmap.org/search';
const NOMINATIM_REVERSE_URL = 'https://nominatim.openstreetmap.org/reverse';
// Builds a Google-Places-shaped `address_components` array (each entry
// `{ long_name, short_name, types: [] }`) from Nominatim's flat `address`
@@ -44,11 +46,99 @@ export const toPlace = (result) => ({
address_components: buildAddressComponents(result.address)
});
// Nominatim requests have no built-in timeout — a stalled connection (proxy
// silently dropping packets, an overloaded public endpoint) would otherwise
// leave `fetch` pending forever, so callers see the spinner spin with nothing
// ever happening. This bounds every request to REQUEST_TIMEOUT_MS.
const REQUEST_TIMEOUT_MS = 8000;
const fetchWithTimeout = (url, options) => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(timer));
};
// Nominatim (and Google Places, previously) both do full-text search against
// a database of named places — neither understands Plus Codes (e.g.
// "5CM7+8Q7"), because a Plus Code isn't a name to look up, it's lat/lng
// encoded directly into the string. Decoding it is a pure local computation
// via the Open Location Code algorithm, so this resolves reliably even where
// OSM has zero data for the area (small towns, unlisted businesses).
const olc = new OpenLocationCode();
const PLUS_CODE_CHARS = '23456789CFGHJMPQRVWX';
const PLUS_CODE_RE = new RegExp(`[${PLUS_CODE_CHARS}]{2,8}\\+[${PLUS_CODE_CHARS}]{2,3}`, 'i');
// A decoded Plus Code is just coordinates — on its own it has no street/
// locality name to show the operator, only the code itself. Reverse-geocoding
// those coordinates fills in the human-readable side (road, suburb, city...)
// the same way a normal search result would, so the option in the dropdown
// reads as an actual address instead of a bare code. Best-effort: if Nominatim
// has no data for that exact point either, callers fall back to the code text.
async function reverseGeocode(lat, lon) {
try {
const params = new URLSearchParams({ lat: String(lat), lon: String(lon), format: 'json', addressdetails: '1' });
const res = await fetchWithTimeout(`${NOMINATIM_REVERSE_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } });
if (!res.ok) return null;
const result = await res.json();
if (!result || result.error) return null;
return result;
} catch (err) {
return null;
}
}
// Short codes (the common case — "5CM7+8Q7" rather than the full
// "6JWV5CM7+8Q7") only encode an offset; recovering the actual location
// needs a nearby reference point, which is what `bias` (the operator's
// tenant/app location) provides. Returns a Nominatim-result-shaped object
// (same shape `toPlace()` expects) so it can flow through the exact same
// path as a real search hit, or null if no code is present / it can't be
// resolved without a reference point.
const plusCodeToResult = async (text, bias) => {
const match = text?.match(PLUS_CODE_RE);
if (!match) return null;
const code = match[0].toUpperCase();
if (!olc.isValid(code)) return null;
try {
let fullCode = code;
if (olc.isShort(code)) {
const refLat = Number(bias?.lat);
const refLng = Number(bias?.lng);
// (0,0) isn't a plausible tenant location — it's what `Number('')` and
// `Number(undefined)` half-produce (empty string coerces to 0), so treat
// it as "no reference point yet" rather than decoding against Null Island.
if (!Number.isFinite(refLat) || !Number.isFinite(refLng) || (refLat === 0 && refLng === 0)) return null;
fullCode = olc.recoverNearest(code, refLat, refLng);
}
if (!olc.isFull(fullCode)) return null;
const { latitudeCenter, longitudeCenter } = olc.decode(fullCode);
// The decoded centroid is exact — keep it as the authoritative coordinate
// even though `reverse` below carries its own (possibly snapped-to-nearest-
// feature) lat/lon; only its display_name/address are what we want.
const reverse = await reverseGeocode(latitudeCenter, longitudeCenter);
return {
...reverse,
place_id: `pluscode-${fullCode}`,
lat: String(latitudeCenter),
lon: String(longitudeCenter),
display_name: reverse?.display_name ? `${reverse.display_name} (near ${code})` : `${code} (Plus Code location)`,
address: reverse?.address || {}
};
} catch (err) {
return null;
}
};
const buildParams = (query, bias) => {
const params = new URLSearchParams({ q: query, format: 'json', addressdetails: '1', limit: '5' });
if (bias?.lat && bias?.lng) {
// bias.lat/lng arrive as strings from the tenant-location API. `-` coerces to
// number automatically, but `+` on a string concatenates instead of adding
// (e.g. "8.17445" + 0.5 === "8.174450.5"), producing a malformed, non-numeric
// viewbox that Nominatim rejects with 400 Bad Request on every single search.
const lat = Number(bias?.lat);
const lng = Number(bias?.lng);
if (Number.isFinite(lat) && Number.isFinite(lng)) {
const d = 0.5; // ~55km box around the operator's zone — soft bias, not a hard filter
params.set('viewbox', `${bias.lng - d},${bias.lat + d},${bias.lng + d},${bias.lat - d}`);
params.set('viewbox', `${lng - d},${lat + d},${lng + d},${lat - d}`);
}
return params;
};
@@ -59,10 +149,12 @@ const buildParams = (query, bias) => {
// or null if nothing matched.
export async function geocodeAddress(address, { bias } = {}) {
if (!address) return null;
const plusCodeResult = await plusCodeToResult(address, bias);
if (plusCodeResult) return toPlace(plusCodeResult);
try {
const params = buildParams(address, bias);
params.set('limit', '1');
const res = await fetch(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } });
const res = await fetchWithTimeout(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } });
if (!res.ok) return null;
const results = await res.json();
if (!results?.length) return null;
@@ -115,10 +207,13 @@ const AddressAutocomplete = ({
return;
}
const params = buildParams(query, bias1);
fetch(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } })
const searchPromise = fetchWithTimeout(`${NOMINATIM_SEARCH_URL}?${params.toString()}`, { headers: { Accept: 'application/json' } })
.then((res) => (res.ok ? res.json() : []))
.then((results) => callback(results || []))
.catch(() => callback([]));
.catch(() => []);
const plusCodePromise = plusCodeToResult(query, bias1).catch(() => null);
Promise.all([searchPromise, plusCodePromise]).then(([results, plusCodeResult]) => {
callback(plusCodeResult ? [plusCodeResult, ...(results || [])] : results || []);
});
}, 500),
[]
);
@@ -153,6 +248,13 @@ const AddressAutocomplete = ({
filterOptions={(x) => x}
getOptionLabel={(option) => (typeof option === 'string' ? option : option.display_name || '')}
isOptionEqualToValue={(option, val) => option.place_id === val?.place_id}
noOptionsText={
loading
? 'Searching…'
: inputValue.trim().length >= 3
? 'No match found — drop the business name/Plus Code, or try just City + Postcode'
: 'Type an address to search'
}
inputValue={inputValue}
onInputChange={(event, newInputValue) => {
setInputValue(newInputValue);