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

12
package-lock.json generated
View File

@@ -39,6 +39,7 @@
"lodash": "^4.17.21",
"mui-daterange-picker": "^1.0.5",
"notistack": "^3.0.1",
"open-location-code": "^1.0.3",
"papaparse": "^5.5.3",
"process": "^0.11.10",
"prop-types": "^15.8.1",
@@ -14285,6 +14286,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/open-location-code": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/open-location-code/-/open-location-code-1.0.3.tgz",
"integrity": "sha512-DBm14BSn40Ee241n80zIFXIT6+y8Tb0I+jTdosLJ8Sidvr2qONvymwqymVbHV2nS+1gkDZ5eTNpnOIVV0Kn2fw==",
"license": "Apache-2.0"
},
"node_modules/optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
@@ -30691,6 +30698,11 @@
"is-wsl": "^2.2.0"
}
},
"open-location-code": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/open-location-code/-/open-location-code-1.0.3.tgz",
"integrity": "sha512-DBm14BSn40Ee241n80zIFXIT6+y8Tb0I+jTdosLJ8Sidvr2qONvymwqymVbHV2nS+1gkDZ5eTNpnOIVV0Kn2fw=="
},
"optionator": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",

View File

@@ -34,6 +34,7 @@
"lodash": "^4.17.21",
"mui-daterange-picker": "^1.0.5",
"notistack": "^3.0.1",
"open-location-code": "^1.0.3",
"papaparse": "^5.5.3",
"process": "^0.11.10",
"prop-types": "^15.8.1",

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);

View File

@@ -25,7 +25,7 @@ import MainCard from 'components/MainCard';
import axios from 'axios';
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
import AddressAutocomplete, { geocodeAddress } from 'components/nearle_components/AddressAutocomplete';
import Loader from 'components/Loader';
import { enqueueSnackbar } from 'notistack';
import { useNavigate } from 'react-router';
@@ -140,6 +140,30 @@ const CreateCustomer = () => {
longitude: place.geometry.location.lng()
});
};
// ============================================= || Fallback geocode for manually typed address || =============================================
// AddressAutocomplete only resolves lat/lng when a suggestion is actually clicked
// from its dropdown (handlePickPlaceSelected). The common real-world case is an
// operator typing/pasting the full address into that single Address box and never
// clicking a suggestion — that left startPoint at (0,0) forever, and because the
// submit validation below used to check pickCust.latitude for an empty string
// that's never actually set, customers were saved with latitude/longitude "0"/"0"
// with no warning at all. This covers both the raw Address-box text and the
// manually filled Location/City/Postcode fields as a fallback, reusing the same
// handler a real dropdown selection would call so the rest of the fields fill in too.
useEffect(() => {
if (startPoint.latitude !== 0 || startPoint.longitude !== 0) return undefined;
const manualFields = [doorno, pickCust.suburb, pickCust.city, pickCust.state, pickCust.postcode].filter(Boolean).join(', ');
const manualAddress =
(inputValue2 && inputValue2.trim().length >= 6 && inputValue2) || (pickCust.suburb && pickCust.city && manualFields);
if (!manualAddress) return undefined;
const timer = setTimeout(async () => {
const place = await geocodeAddress(manualAddress, { bias: { lat: appLocaLat, lng: appLocaLng } });
if (place) handlePickPlaceSelected(place);
}, 800);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputValue2, doorno, pickCust.suburb, pickCust.city, pickCust.state, pickCust.postcode, appLocaLat, appLocaLng]);
// ==================================================== || getapplocations || ====================================================
const getapplocations = async () => {
setLoading(true);
@@ -554,10 +578,8 @@ const CreateCustomer = () => {
opentoast('Enter Post Code ');
} else if (landmark === '') {
opentoast('Enter Land Mark ');
} else if (pickCust.latitude === '') {
opentoast('Invalid latitude ');
} else if (pickCust.longitude === '') {
opentoast('Invaiid Longitude ');
} else if (!startPoint.latitude || !startPoint.longitude) {
opentoast('Could not locate address on map, please recheck the address');
} else {
createprofile();
}

View File

@@ -46,7 +46,7 @@ import { TbMapPinCode } from 'react-icons/tb';
import { FaLocationDot } from 'react-icons/fa6';
import axios from 'axios';
import { useTheme } from '@mui/material/styles';
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
import AddressAutocomplete, { geocodeAddress } from 'components/nearle_components/AddressAutocomplete';
import Loader from 'components/Loader';
import * as geolib from 'geolib';
import MainCard from 'components/MainCard';
@@ -578,10 +578,10 @@ const Createorder1 = () => {
deliverycustomer: dropCust.firstname || '',
deliveryid: isNumChange2 == 0 ? +dropCust.customerid || 0 : 0,
deliverylandmark: dropCust.landmark || '',
deliverylat: dropCust.latitude.toString(),
deliverylat: (dropCust.latitude || 0).toString(),
deliverylocation: dropCust.suburb || '',
deliverylocationid: dropCust.deliverylocationid || 0,
deliverylong: dropCust.longitude.toString(),
deliverylong: (dropCust.longitude || 0).toString(),
deliverytime: `${dayjs(startdate).format('YYYY-MM-DD')} ${dayjs(selectedtime.$d).format('HH:mm:ss')}`,
deliverytype: pickCust.customerid !== 0 || dropCust.customerid !== 0 ? 'B' : 'C',
delivered: '',
@@ -606,10 +606,10 @@ const Createorder1 = () => {
pickupcontactno: pickCust.contactno || '',
pickupcustomer: pickCust.firstname || '',
pickuplandmark: pickCust.landmark || '',
pickuplat: pickCust.latitude.toString(),
pickuplat: (pickCust.latitude || 0).toString(),
pickuplocation: pickCust.suburb || '',
pickuplocationid: pickCust.deliverylocationid || 0,
pickuplong: pickCust.longitude.toString(),
pickuplong: (pickCust.longitude || 0).toString(),
processing: '',
ready: '',
remarks: '',
@@ -637,8 +637,8 @@ const Createorder1 = () => {
email: pickCust.email || '',
firstname: pickCust.firstname || '',
landmark: pickCust.landmark || '',
latitude: pickCust.latitude.toString() || '',
longitude: pickCust.longitude.toString() || '',
latitude: (pickCust.latitude || 0).toString(),
longitude: (pickCust.longitude || 0).toString(),
locationid: pickCust.deliverylocationid || 0,
postcode: pickCust.postcode || '',
primaryaddress: 1,
@@ -663,8 +663,8 @@ const Createorder1 = () => {
email: dropCust.email || '',
firstname: dropCust.firstname || '',
landmark: dropCust.landmark || '',
latitude: dropCust.latitude.toString(),
longitude: dropCust.longitude.toString(),
latitude: (dropCust.latitude || 0).toString(),
longitude: (dropCust.longitude || 0).toString(),
locationid: dropCust.deliverylocationid || 0,
postcode: dropCust.postcode || '',
primaryaddress: 1,
@@ -689,6 +689,8 @@ const Createorder1 = () => {
opentoast('Enter Pickup Postcode ', 'warning', 2000);
} else if (!pickCust.landmark) {
opentoast('Enter Pickup Landmark ', 'warning', 2000);
} else if (!pickCust.latitude || !pickCust.longitude) {
opentoast('Could not locate Pickup address on map, please recheck the address', 'error', 3000);
} else if (!dropCust.firstname) {
opentoast('Enter Drop Contact Name ', 'warning', 2000);
} else if (!dropCust.contactno) {
@@ -703,6 +705,8 @@ const Createorder1 = () => {
opentoast('Enter Drop postcode ', 'warning', 2000);
} else if (!dropCust.landmark) {
opentoast('Enter Drop Landmark ', 'warning', 2000);
} else if (!dropCust.latitude || !dropCust.longitude) {
opentoast('Could not locate Drop address on map, please recheck the address', 'error', 3000);
} else if (!selectedtime) {
opentoast('Choose deliverytime ', 'warning', 2000);
} else if (!setSubCatId) {
@@ -991,6 +995,45 @@ const Createorder1 = () => {
});
};
// ============================================= || Fallback geocode for manually typed addresses || =============================================
// AddressAutocomplete only resolves lat/lng when a suggestion is actually clicked
// from its dropdown (handlePickPlaceSelected/handleDropPlaceSelected). The common
// real-world case is an operator typing/pasting the full address into that single
// Address box and never clicking a suggestion (none showed up, or they just hit
// tab/moved on) — that left startPoint/endPoint at (0,0) forever, distance never
// calculated, and "Select Pickup and Drop" firing no matter what was typed.
// These effects cover that: once typing pauses, whatever address text exists —
// the raw Address-box text, or the manually filled Door No/Location/City/Postcode
// fields as a fallback — gets geocoded, reusing the same handler that a real
// dropdown selection would call so suburb/city/postcode get filled in too.
useEffect(() => {
if (startPoint.latitude !== 0 || startPoint.longitude !== 0) return undefined;
const manualAddress =
(inputValue2 && inputValue2.trim().length >= 6 && inputValue2) ||
(pickCust.suburb && pickCust.city && [pickCust.doorno, pickCust.suburb, pickCust.city, pickCust.postcode].filter(Boolean).join(', '));
if (!manualAddress) return undefined;
const timer = setTimeout(async () => {
const place = await geocodeAddress(manualAddress, { bias: { lat: appLocaLat, lng: appLocaLng } });
if (place) handlePickPlaceSelected(place);
}, 800);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputValue2, pickCust.doorno, pickCust.suburb, pickCust.city, pickCust.postcode, appLocaLat, appLocaLng]);
useEffect(() => {
if (endPoint.latitude !== 0 || endPoint.longitude !== 0) return undefined;
const manualAddress =
(inputValue3 && inputValue3.trim().length >= 6 && inputValue3) ||
(dropCust.suburb && dropCust.city && [dropCust.doorno, dropCust.suburb, dropCust.city, dropCust.postcode].filter(Boolean).join(', '));
if (!manualAddress) return undefined;
const timer = setTimeout(async () => {
const place = await geocodeAddress(manualAddress, { bias: { lat: appLocaLat, lng: appLocaLng } });
if (place) handleDropPlaceSelected(place);
}, 800);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [inputValue3, dropCust.doorno, dropCust.suburb, dropCust.city, dropCust.postcode, appLocaLat, appLocaLng]);
// ============================================= || gettenantlocations (branches) || =============================================
const gettenantlocations = async () => {
try {
@@ -1749,7 +1792,7 @@ const Createorder1 = () => {
placeholder="Select"
value={dropCust.address}
onChange={(e) => {
setPickCust({ ...dropCust, address: e.target.value });
setDropCust({ ...dropCust, address: e.target.value });
if (e.target.value == '') {
setAddId2(0);
setShowDistance(false);

View File

@@ -7798,6 +7798,11 @@ onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"
open-location-code@^1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/open-location-code/-/open-location-code-1.0.3.tgz"
integrity sha512-DBm14BSn40Ee241n80zIFXIT6+y8Tb0I+jTdosLJ8Sidvr2qONvymwqymVbHV2nS+1gkDZ5eTNpnOIVV0Kn2fw==
open@^8.0.9, open@^8.4.0:
version "8.4.0"
resolved "https://registry.npmjs.org/open/-/open-8.4.0.tgz"