updates on the admin and zone things and minor changes
This commit is contained in:
@@ -23,10 +23,16 @@ export function ZoneProvider({ children }) {
|
||||
const tenantId = user?.tenantid || localStorage.getItem('tenantid') || '';
|
||||
const isTenantUser = Boolean(tenantId && tenantId !== '0');
|
||||
|
||||
// Staff loads global hubs; Tenant loads tenant locations / kitchen hubs
|
||||
const { data: rawHubs = [], isLoading: isHubsLoading, refetch: refetchHubs } = useHubs({
|
||||
enabled: !isTenantUser,
|
||||
});
|
||||
// A client login's city (from the login response), used to offer them the
|
||||
// Doormile hubs of their own city. Missing on a session signed in before the
|
||||
// backend sent it, in which case every active hub is offered.
|
||||
const clientCityId = user?.applocationid != null && String(user.applocationid) !== '0' ? String(user.applocationid) : '';
|
||||
|
||||
// Staff see Doormile's hubs. A client sees their own locations (kitchens,
|
||||
// depots) AND the Doormile hubs of their city: a newly onboarded client had
|
||||
// no locations at all, so their zone list was just "All Zones" and the
|
||||
// Coimbatore hub their orders run through never appeared.
|
||||
const { data: rawHubs = [], isLoading: isHubsLoading, refetch: refetchHubs } = useHubs();
|
||||
const { data: tenantLocations = [], isLoading: isLocationsLoading, refetch: refetchLocations } = useTenantLocations(
|
||||
isTenantUser ? tenantId : null,
|
||||
{ enabled: isTenantUser }
|
||||
@@ -41,10 +47,21 @@ export function ZoneProvider({ children }) {
|
||||
});
|
||||
|
||||
const activeHubs = useMemo(() => {
|
||||
const doormileHubs = (rawHubs || []).filter(
|
||||
(hub) => String(hub.status || 'Active').toLowerCase() === 'active'
|
||||
);
|
||||
if (isTenantUser) {
|
||||
return (tenantLocations || []).map((loc) => ({
|
||||
hubid: String(loc.tenantlocationid || loc.locationid || loc.id),
|
||||
id: String(loc.tenantlocationid || loc.locationid || loc.id),
|
||||
// A client location's id and a hub id are separate numberings, so the
|
||||
// location's zone id is prefixed; `locationid` keeps the raw one for
|
||||
// matching orders (matchesZone).
|
||||
// A deactivated location is no longer a place the client ships from;
|
||||
// it was listed (and labelled Active) regardless until now.
|
||||
const own = (tenantLocations || [])
|
||||
.filter((loc) => String(loc.status || 'Active').toLowerCase() === 'active')
|
||||
.map((loc) => ({
|
||||
hubid: `loc:${loc.tenantlocationid || loc.locationid || loc.id}`,
|
||||
id: `loc:${loc.tenantlocationid || loc.locationid || loc.id}`,
|
||||
locationid: String(loc.tenantlocationid || loc.locationid || loc.id),
|
||||
hubname: loc.locationname || loc.name || `Kitchen Hub #${loc.tenantlocationid || loc.locationid}`,
|
||||
city: loc.city || '',
|
||||
address: loc.address || '',
|
||||
@@ -55,12 +72,14 @@ export function ZoneProvider({ children }) {
|
||||
isTenantLocation: true,
|
||||
status: 'Active',
|
||||
}));
|
||||
const cityHubs = clientCityId
|
||||
? doormileHubs.filter((hub) => String(hub.applocationid ?? '') === clientCityId)
|
||||
: doormileHubs;
|
||||
return [...own, ...cityHubs];
|
||||
}
|
||||
|
||||
return (rawHubs || []).filter(
|
||||
(hub) => String(hub.status || 'Active').toLowerCase() === 'active'
|
||||
);
|
||||
}, [isTenantUser, tenantLocations, rawHubs]);
|
||||
return doormileHubs;
|
||||
}, [isTenantUser, tenantLocations, rawHubs, clientCityId]);
|
||||
|
||||
const zones = useMemo(() => {
|
||||
return [ALL_ZONE, ...activeHubs];
|
||||
@@ -178,10 +197,15 @@ export function ZoneProvider({ children }) {
|
||||
// The old `??` chain hid this by stopping at the first non-null field, so
|
||||
// the collision only fired when the earlier ones happened to be absent. It
|
||||
// was a bug waiting on its inputs, not a safe design.
|
||||
const itemHubIds = isTenantUser
|
||||
//
|
||||
// What decides the id space is the selected ZONE, not who is signed in: a
|
||||
// client can now pick one of their own locations (compare location ids)
|
||||
// or a Doormile hub (compare hub ids).
|
||||
const itemHubIds = currentHub.isTenantLocation
|
||||
? [item.tenantlocationid, item.locationid]
|
||||
: [item.servicinghubid, item.pickuphubid, item.nearesthubid, item.hubid, item.sourcehubid];
|
||||
if (itemHubIds.some((id) => id != null && String(id) === targetHubId)) {
|
||||
const targetId = currentHub.isTenantLocation ? String(currentHub.locationid) : targetHubId;
|
||||
if (itemHubIds.some((id) => id != null && String(id) === targetId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -232,8 +256,13 @@ export function ZoneProvider({ children }) {
|
||||
isAllZones,
|
||||
isTenantUser,
|
||||
tenantId,
|
||||
isHubsLoading: isTenantUser ? isLocationsLoading : isHubsLoading,
|
||||
refetchHubs: isTenantUser ? refetchLocations : refetchHubs,
|
||||
isHubsLoading: isTenantUser ? isLocationsLoading || isHubsLoading : isHubsLoading,
|
||||
refetchHubs: isTenantUser
|
||||
? () => {
|
||||
refetchLocations();
|
||||
refetchHubs();
|
||||
}
|
||||
: refetchHubs,
|
||||
matchesZone,
|
||||
}),
|
||||
[
|
||||
|
||||
@@ -53,9 +53,75 @@ export function validateOnboarding(form) {
|
||||
errors.password = 'Must not be the email or the phone number';
|
||||
}
|
||||
if (form.confirm !== pw) errors.confirm = 'The passwords do not match';
|
||||
Object.assign(errors, validateAddress(form));
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* The client's main address. Mirrors the server: an address, a 6-digit
|
||||
* pincode, and map coordinates — which only come from picking a suggestion,
|
||||
* so typed text alone is refused.
|
||||
*/
|
||||
export function validateAddress(form) {
|
||||
const errors = {};
|
||||
if (String(form.address || '').trim().length < 5) errors.address = "Enter the client's address";
|
||||
else if (String(form.address).trim().length > 300) errors.address = 'At most 300 characters';
|
||||
else if (!hasMapLocation(form)) errors.address = 'Pick the address from the suggestions so it has a map location';
|
||||
if (!/^[1-9]\d{5}$/.test(String(form.pincode || '').replace(/\s/g, ''))) errors.pincode = 'Enter a 6-digit pincode';
|
||||
return errors;
|
||||
}
|
||||
|
||||
/** Real coordinates inside India (zero means nothing was picked). */
|
||||
export function hasMapLocation(form) {
|
||||
const lat = Number(form.latitude);
|
||||
const lng = Number(form.longitude);
|
||||
return Number.isFinite(lat) && Number.isFinite(lng) && lat >= 6 && lat <= 37.5 && lng >= 68 && lng <= 97.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* The address fields from a place picked in AddressAutocomplete (the same
|
||||
* shapes the order form reads: our own fields first, Google-style
|
||||
* address_components as a fallback).
|
||||
*/
|
||||
export function addressFromPlace(place) {
|
||||
if (!place) return {};
|
||||
const parts = { city: '', state: '', pincode: '' };
|
||||
(place.address_components || []).forEach((c) => {
|
||||
(c.types || []).forEach((type) => {
|
||||
if (type === 'locality') parts.city = c.long_name;
|
||||
if (type === 'administrative_area_level_1') parts.state = c.long_name;
|
||||
if (type === 'postal_code') parts.pincode = c.long_name;
|
||||
});
|
||||
});
|
||||
const g = place.geometry?.location;
|
||||
const read = (v) => (typeof v === 'function' ? v() : v);
|
||||
const lat = place.latitude != null ? place.latitude : read(g?.lat);
|
||||
const lng = place.longitude != null ? place.longitude : read(g?.lng);
|
||||
const out = {
|
||||
address: place.formatted_address || place.name || '',
|
||||
city: place.city || parts.city,
|
||||
state: place.state || parts.state,
|
||||
pincode: String(place.postcode || parts.pincode || '').replace(/\s/g, ''),
|
||||
};
|
||||
if (lat != null && lng != null) {
|
||||
out.latitude = Number(lat);
|
||||
out.longitude = Number(lng);
|
||||
}
|
||||
return Object.fromEntries(Object.entries(out).filter(([, v]) => v !== '' && v != null));
|
||||
}
|
||||
|
||||
/** The address part of a form, as the API takes it. */
|
||||
export function addressPayload(form) {
|
||||
return {
|
||||
address: String(form.address || '').trim(),
|
||||
city: String(form.city || '').trim(),
|
||||
state: String(form.state || '').trim(),
|
||||
pincode: String(form.pincode || '').replace(/\s/g, ''),
|
||||
latitude: Number(form.latitude),
|
||||
longitude: Number(form.longitude),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Why onboarding cannot run, from the failed GET /admin/clients/cities. The
|
||||
* page shows this instead of an empty city list: the likeliest cause is a
|
||||
|
||||
37
src/lib/consoleNav.js
Normal file
37
src/lib/consoleNav.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Which header-menu groups a login sees.
|
||||
*
|
||||
* Kept apart from AdminLayout so the rule can be tested without rendering the
|
||||
* whole shell.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Fleet Ops pages a client (tenant) login may open. The server scopes each
|
||||
* of these to the client: hubs to its own city, exceptions and returns to its
|
||||
* own parcels. Everything else under Fleet Ops is internal or would show other
|
||||
* clients' data, and the server refuses a client token on it.
|
||||
*/
|
||||
export const CLIENT_FLEET_OPS_PATHS = ['/doormile/hubs', '/doormile/exceptions', '/doormile/returns'];
|
||||
|
||||
export const CLIENT_ONBOARDING_PATH = '/doormile/clients/onboard';
|
||||
|
||||
/**
|
||||
* @param {Array<{label: string, items: Array<{path: string}>}>} groups the full menu
|
||||
* @param {{isClient: boolean, canOnboard: boolean, onboardingItem: object}} who
|
||||
*/
|
||||
export function navGroupsFor(groups, { isClient, canOnboard, onboardingItem }) {
|
||||
if (isClient) {
|
||||
// Never Agents or Client Onboarding: those are Doormile-only.
|
||||
return groups.map((group) =>
|
||||
group.label === 'Fleet Ops'
|
||||
? { ...group, items: group.items.filter((item) => CLIENT_FLEET_OPS_PATHS.includes(item.path)) }
|
||||
: group
|
||||
);
|
||||
}
|
||||
if (!canOnboard) return groups;
|
||||
// Only the onboarding owner login sees this entry; the server refuses
|
||||
// everyone else regardless.
|
||||
return groups.map((group) =>
|
||||
group.label === 'Fleet Ops' ? { ...group, items: [...group.items, onboardingItem] } : group
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user