+
-
);
diff --git a/src/menu-items/nearle.js b/src/menu-items/nearle.js
index 2d82fcc..5d58751 100644
--- a/src/menu-items/nearle.js
+++ b/src/menu-items/nearle.js
@@ -31,7 +31,8 @@ import {
IdcardOutlined,
TruckOutlined,
WarningOutlined,
- RadarChartOutlined
+ RadarChartOutlined,
+ RiseOutlined
} from '@ant-design/icons';
// icons
@@ -64,7 +65,8 @@ const icons = {
IdcardOutlined,
TruckOutlined,
WarningOutlined,
- RadarChartOutlined
+ RadarChartOutlined,
+ RiseOutlined
};
// ==============================|| MENU ITEMS - SUPPORT ||============================== //
@@ -200,6 +202,13 @@ const nearle = {
type: 'item',
url: '/doormile/reports/riderssummary',
icon: icons.UserOutlined
+ },
+ {
+ id: 'profitability',
+ title:
,
+ type: 'item',
+ url: '/doormile/reports/profitability',
+ icon: icons.RiseOutlined
}
]
}
diff --git a/src/pages/api/api.js b/src/pages/api/api.js
index 07a7fb1..300e4c0 100644
--- a/src/pages/api/api.js
+++ b/src/pages/api/api.js
@@ -1,5 +1,6 @@
import axios from 'axios';
import { OpenToast } from 'components/third-party/OpenToast';
+import logger from 'utils/logger';
import {
getMilers,
getMiler,
@@ -14,13 +15,10 @@ import {
getBooking,
assignMilerToBooking,
cancelBooking,
- bulkCancelBookings,
updateConsignmentStatus,
getAdminPricing,
- getPartners,
getReports,
getLocationsSummary,
- getDashboard,
getProfile
} from 'pages/api/doormileApi';
@@ -88,8 +86,10 @@ export const getRiderPeriodicLogs = async (userid) => {
const logs = await getMilerLogs(userid, undefined, undefined, 1);
const latest = Array.isArray(logs) ? logs[0] : logs;
if (latest) return latest;
- } catch (err) {
- console.log('getRiderPeriodicLogs', err.message);
+ } catch {
+ // Falls through to the getMiler() fallback below โ this is polled every
+ // 15s by the live map, so a toast here would spam the operator on every
+ // failed tick. Only surface an error if the fallback also fails.
}
try {
return (await getMiler(userid)) || null;
@@ -122,25 +122,35 @@ export const fetchAppLocations = async () => {
// the doc only promises "counts + today's numbers", not an exact schema.
export const fetchPercentageData = async ({ queryKey }) => {
const [, , startdate, enddate] = queryKey;
- const details = (await getReports(startdate, enddate)) || {};
- const total = details.total || 0;
- return {
- created: String(details.created || 0),
- uncoveredOrders: String(details.pending || 0),
- coveredOrders: String(details.delivered || 0),
- cancelled: String(details.cancelled || 0),
- percentage1: String(Math.round(((details.created || 0) / total) * 100) || 0),
- percentage2: String(Math.round(((details.pending || 0) / total) * 100) || 0),
- percentage3: String(Math.round(((details.delivered || 0) / total) * 100) || 0),
- percentage4: String(Math.round(((details.cancelled || 0) / total) * 100) || 0)
- };
+ try {
+ const details = (await getReports(startdate, enddate)) || {};
+ const total = details.total || 0;
+ return {
+ created: String(details.created || 0),
+ uncoveredOrders: String(details.pending || 0),
+ coveredOrders: String(details.delivered || 0),
+ cancelled: String(details.cancelled || 0),
+ percentage1: String(Math.round(((details.created || 0) / total) * 100) || 0),
+ percentage2: String(Math.round(((details.pending || 0) / total) * 100) || 0),
+ percentage3: String(Math.round(((details.delivered || 0) / total) * 100) || 0),
+ percentage4: String(Math.round(((details.cancelled || 0) / total) * 100) || 0)
+ };
+ } catch (err) {
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load order percentages', 'error', 2000);
+ return { created: '0', uncoveredOrders: '0', coveredOrders: '0', cancelled: '0', percentage1: '0', percentage2: '0', percentage3: '0', percentage4: '0' };
+ }
};
// ===================================================== || getTenants || =====================================================
export const getTenants = async () => {
- const tenants = await getAdminTenants();
- return (tenants || []).map((val) => ({ ...val, label: val.tenantname }));
+ try {
+ const tenants = await getAdminTenants();
+ return (tenants || []).map((val) => ({ ...val, label: val.tenantname }));
+ } catch (err) {
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load tenants', 'error', 2000);
+ return [];
+ }
};
// ============================================= || gettenantlocations (branches) || =============================================
@@ -148,31 +158,14 @@ export const gettenantlocations = async (tenantId) => {
try {
return (await getTenantLocations(tenantId)) || [];
} catch (err) {
- console.log('gettenantlocations', err);
+ // Log only the message โ the raw error object carries the request
+ // config, including the Authorization: Bearer
header
+ // doormileAxios attaches, which shouldn't hit the console.
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load tenant locations', 'error', 2000);
return [];
}
};
-// ==============================|| fetchorderscount (orders) ||============================== //
-
-export const fetchorderscount = async ({ queryKey }) => {
- const [, , startdate, enddate] = queryKey;
- return (await getReports(startdate, enddate)) || {};
-};
-
-// ==============================|| fetchOrders (orders) ||============================== //
-// Maps to /admin/bookings. The old endpoint's applocationid/tenantid/locationid
-// filters and free-text `status` are not documented on this resource โ pageno/
-// pagesize are (see "Conventions" in express-console-api.md).
-export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
- const [, , , , , , rowsPerPage] = queryKey;
- const rows = (await getBookings(pageParam, rowsPerPage)) || [];
- return {
- rows,
- nextPage: rows.length === Number(rowsPerPage) ? pageParam + 1 : undefined
- };
-};
-
// ==============================|| fetchPaymentType (orders) ||============================== //
// No payment-mode-types endpoint in the new API.
export const fetchPaymentType = async () => [];
@@ -187,8 +180,14 @@ export const fetchRidersList = async () => {
label: `${val.displayname || val.authname || ''} | ${val.contactno || ''}`
}));
} catch (err) {
- OpenToast(err.message, 'error', 2000);
- throw err;
+ // Was `throw`ing after already toasting here โ deliveries.js also wires
+ // its own onError toast on this same query, so a real failure showed
+ // the operator the same error twice. Matches every sibling function's
+ // convention now: toast once, return a safe default, never throw (the
+ // other 4 call sites had no onError of their own and got zero feedback
+ // on failure before this fix).
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load riders', 'error', 2000);
+ return [];
}
};
@@ -217,21 +216,128 @@ export const fetchBatchEfficiency = async ({ batch, tenantId }) => {
return response.data;
};
// ==============================|| finalCreatedeliveries (orders) ||============================== //
-// Final delivery commit โ also part of the separate solver/optimiser pipeline,
-// left untouched (still posts to jupiter.nearle.app directly, not via
-// REACT_APP_URL, so this migration doesn't need to change it).
+// Final delivery commit. Used to POST the whole batch to jupiter's
+// /deliveries/createdeliveries in one shot โ retired for two confirmed
+// reasons: (1) it 500'd because jupiter's Deliveries Go struct types
+// pickuplat/pickuplon/droplat/droplon as string, while GET /admin/bookings
+// (the new Doormile API) feeds orders.js's solver payload with those as
+// numbers; (2) even once that's patched, jupiter writes into jupiter's own
+// now-orphaned database โ neither the Orders "pending" list nor the
+// Deliveries "dispatched" filter read it, since both come from Doormile's
+// GET /admin/bookings. jupiter2doormile.md ยง4 (the "assign a rider" row,
+// mapped from jupiter's old PUT /deliveries/updatedelivery) and
+// express-console-api.md's exercised-routes list both confirm
+// POST /admin/bookings/:id/assign-miler as the correct, live-proven
+// replacement for this exact action โ not POST /admin/expressbooking
+// (that one creates a brand-new booking from scratch; wrong here, since
+// every order already exists as a Doormile booking pulled from
+// GET /admin/bookings).
export const finalCreatedeliveries = async (deliveryData) => {
- const toInt = (v) => {
- const n = Number(v);
- return Number.isFinite(n) ? n : v;
- };
- const deliveries = (deliveryData.deliveries || []).map((d) => ({
- ...d,
- userid: toInt(d.userid),
- rider_id: toInt(d.rider_id)
- }));
- const response = await axios.post(`https://jupiter.nearle.app/live/api/v1/deliveries/createdeliveries`, deliveries);
- return response.data;
+ const deliveries = deliveryData.deliveries || [];
+ logger.debug(`finalCreatedeliveries: ${deliveries.length} order(s) to assign`);
+ if (!deliveries.length) return { success: true, assigned: 0, failed: 0 };
+
+ // Diagnostic only (no PII beyond a rider's display name, which every
+ // operator already sees in the UI). Shows exactly which candidate field
+ // names are actually present, so a wrong guess in the fallback chain
+ // below is visible instead of silently mismatching.
+ deliveries.forEach((d, i) => {
+ logger.debug(` order[${i}] id fields:`, {
+ bookingid: d.bookingid,
+ orderheaderid: d.orderheaderid,
+ deliveryid: d.deliveryid,
+ orderid: d.orderid,
+ rider_id: d.rider_id,
+ userid: d.userid,
+ rider_name: d.rider_name ?? d.rider
+ });
+ });
+
+ // assign-miler needs a milerprofileid. userid/milerprofileid matching was
+ // tried first (the solver's rider pool is fed full miler objects โ see
+ // Preview.js's handleCreateDelivery -> `riders: autoRiders`, raw
+ // GET /admin/milers data carrying both fields per rider) but confirmed
+ // live that NEITHER matches: the solver's rider_id/userid (e.g. "883")
+ // isn't in GET /admin/milers under either field. This matches
+ // Dispatch.js's own Analysis-panel comment โ "the workolik solver doesn't
+ // have our auth/users table" โ so its numeric rider ids are its own
+ // internal numbering, disconnected from Doormile's id space entirely.
+ // Falls back to matching the rider's NAME (also echoed by the solver,
+ // see flattenRiders' rider_name) against each miler's displayname/
+ // authname โ the only other correlatable field. Logs the full roster
+ // alongside so a further mismatch is fully diagnosable from one run.
+ let milers = [];
+ try {
+ milers = (await getMilers()) || [];
+ } catch (err) {
+ logger.error('finalCreatedeliveries: GET /admin/milers failed', err.response?.status, err.response?.data || err.message);
+ }
+ logger.debug(
+ `finalCreatedeliveries: ${milers.length} miler(s) available for rider resolution`,
+ milers.map((m) => ({ userid: m.userid, milerprofileid: m.milerprofileid, name: m.displayname || m.authname }))
+ );
+ const milerByUserId = new Map(milers.map((m) => [String(m.userid), m]));
+ const milerByProfileId = new Map(milers.map((m) => [String(m.milerprofileid), m]));
+ const normName = (s) => String(s || '').trim().toLowerCase();
+ const milerByName = new Map();
+ milers.forEach((m) => {
+ [m.displayname, m.authname].forEach((n) => {
+ const key = normName(n);
+ if (key && !milerByName.has(key)) milerByName.set(key, m);
+ });
+ });
+
+ const results = await Promise.allSettled(
+ deliveries.map(async (d) => {
+ // Booking id: the solver is a separate, unverified service (see this
+ // area's CLAUDE.md) โ falls back through every plausible field name
+ // an order might carry it under, `bookingid` first since that's what
+ // orders.js actually sends in and most solvers pass unknown fields
+ // through untouched.
+ const bookingId = d.bookingid ?? d.orderheaderid ?? d.deliveryid ?? d.orderid;
+ const riderUserId = d.rider_id ?? d.userid;
+ const riderName = d.rider_name ?? d.rider;
+ const matchedVia = milerByUserId.has(String(riderUserId))
+ ? 'userid'
+ : milerByProfileId.has(String(riderUserId))
+ ? 'milerprofileid'
+ : milerByName.has(normName(riderName))
+ ? 'name'
+ : null;
+ const rider =
+ milerByUserId.get(String(riderUserId)) ?? milerByProfileId.get(String(riderUserId)) ?? milerByName.get(normName(riderName));
+ if (bookingId == null || !rider?.milerprofileid) {
+ const reason =
+ bookingId == null
+ ? 'no booking id resolved on the order object'
+ : `no miler found for rider id ${riderUserId} / name "${riderName}" (checked userid, milerprofileid, name)`;
+ logger.error(`finalCreatedeliveries: skipping order โ ${reason}`);
+ throw new Error(`order ${d.orderid ?? bookingId ?? '?'}: ${reason}`);
+ }
+ logger.debug(`finalCreatedeliveries: order booking ${bookingId} -> rider ${riderUserId} ("${riderName}") matched via ${matchedVia}`);
+ try {
+ return await assignMilerToBooking(bookingId, { milerid: rider.milerprofileid });
+ } catch (err) {
+ logger.error(
+ `finalCreatedeliveries: assign-miler failed for booking ${bookingId}`,
+ err.response?.status,
+ err.response?.data || err.message
+ );
+ throw err;
+ }
+ })
+ );
+
+ const failed = results.filter((r) => r.status === 'rejected');
+ if (failed.length === deliveries.length) {
+ // Total failure โ throw so the caller's mutation onError fires instead
+ // of a false "success" toast + navigate-away with nothing assigned.
+ throw new Error(`Couldn't assign any of the ${deliveries.length} order(s)`);
+ }
+ if (failed.length) {
+ OpenToast(`${failed.length} of ${deliveries.length} order(s) couldn't be assigned โ check Orders/Deliveries`, 'warning', 4000);
+ }
+ return { success: true, assigned: deliveries.length - failed.length, failed: failed.length };
};
// ==============================|| createAutomationDeliveries (orders) Auto rider Assign ||============================== //
// Also part of the optimiser pipeline (routes.workolik.com / routemate.workolik.com) โ untouched.
@@ -263,17 +369,6 @@ export const notifyRider = async (
return notifyMiler(milerProfileId, title, message);
};
-// ==============================|| cancelOrder (orders) ||============================== //
-
-export const cancelOrder = async (bookingId) => cancelBooking(bookingId);
-
-// ==============================|| cancelMultipleOrder (orders) ||============================== //
-
-export const cancelMultipleOrder = async (orderlist) => {
- const bookingIds = (orderlist || []).map((e) => e.bookingid ?? e.id);
- return bulkCancelBookings(bookingIds);
-};
-
// ==============================|| fetchDeliveries (deliveries) ||============================== //
// GET /admin/consignments has zero documented response schema (see
// express-console-api.md โ it's listed with no example body at all, unlike
@@ -297,8 +392,19 @@ export const cancelMultipleOrder = async (orderlist) => {
// approach as fetchAllRiders' `{details}` wrapper.
export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
const [, , , , , , rowsPerPage] = queryKey;
- const [bookings, customers, milers, tenants] = await Promise.all([
- getBookings(pageParam, rowsPerPage),
+ // Unlike the 3 joins below (customers/milers/tenants โ each individually
+ // guarded so a failed join just degrades a display field, not the whole
+ // page), a failed bookings call is the one thing this function can't
+ // recover from โ the previous version let it reject the whole Promise.all
+ // uncaught, with no toast anywhere across the 4 pages that call this.
+ let bookings;
+ try {
+ bookings = await getBookings(pageParam, rowsPerPage);
+ } catch (err) {
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load deliveries', 'error', 2000);
+ return { rows: [], nextPage: undefined };
+ }
+ const [customers, milers, tenants] = await Promise.all([
getAdminCustomers().catch(() => []),
getMilers().catch(() => []),
getAdminTenants().catch(() => [])
@@ -319,6 +425,7 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
deliveryid: b.bookingid,
orderid: b.bookingno || `#${b.bookingid}`,
consignmentid: b.consignmentid,
+ tenantid: b.tenantid,
tenantname: tenant?.tenantname || '',
tenantsuburb: '',
applocation: '',
@@ -383,38 +490,24 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
};
};
-// Shared by fetchPercentageAPI/fetchCountAPI so the chip counts are derived
-// from the exact same source (and status mapping) as fetchDeliveries' rows โ
-// counting against the separately-guessed /admin/dashboard shape produced
-// numbers that didn't match what the table actually showed.
+// Backs fetchCountAPI so the chip counts are derived from the exact same
+// source (and status mapping) as fetchDeliveries' rows โ counting against
+// the separately-guessed /admin/dashboard shape produced numbers that didn't
+// match what the table actually showed.
const getDeliveryStatusCounts = async () => {
- const bookings = (await getBookings(1, 1000)) || [];
- const dispatched = bookings.filter((b) => b.assignedmileruserid || b.consignmentid);
- const counts = { total: dispatched.length };
- dispatched.forEach((b) => {
- const status = mapBookingStatusToDeliveryStatus(b.status);
- counts[status] = (counts[status] || 0) + 1;
- });
- return counts;
-};
-
-// ==============================|| fetchPercentageAPI (deliveries) ||============================== //
-
-export const fetchPercentageAPI = async () => {
- const data = await getDeliveryStatusCounts();
- return {
- coveredOrders: String(data.delivered || 0),
- cancelledOrders: String(data.cancelled || 0),
- uncoveredOrders: String(data.pending || 0),
- assignedOrders: String(data.accepted || 0),
- createdOrders: String(data.total || 0),
- closedOrders: String(data.delivered || 0),
- pickedOrders: String(data.picked || 0),
- percentage1: String(Math.round(((data.pending || 0) / (data.total || 1)) * 100) || 0),
- percentage2: String(Math.round(((data.accepted || 0) / (data.total || 1)) * 100) || 0),
- percentage3: String(Math.round(((data.picked || 0) / (data.total || 1)) * 100) || 0),
- percentage4: String(Math.round(((data.delivered || 0) / (data.total || 1)) * 100) || 0)
- };
+ try {
+ const bookings = (await getBookings(1, 1000)) || [];
+ const dispatched = bookings.filter((b) => b.assignedmileruserid || b.consignmentid);
+ const counts = { total: dispatched.length };
+ dispatched.forEach((b) => {
+ const status = mapBookingStatusToDeliveryStatus(b.status);
+ counts[status] = (counts[status] || 0) + 1;
+ });
+ return counts;
+ } catch (err) {
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load delivery counts', 'error', 2000);
+ return {};
+ }
};
// ==============================|| fetchCountAPI (deliveries) ||============================== //
@@ -473,21 +566,6 @@ export const getalltenants = async () => {
return null;
}
};
-// ==============================|| gettenantsummary (tenants) ||============================== //
-// No tenant-tab-counts endpoint in the new API.
-export const gettenantsummary = async () => null;
-
-// ==============================|| getpricinglist (tenants) ||============================== //
-
-export const getpricinglist = async () => {
- try {
- return (await getAdminPricing()) || [];
- } catch (err) {
- const message = err.response?.data?.message || err.message || 'Something went wrong';
- OpenToast(message);
- return null;
- }
-};
// ==============================|| getallpricing (clientPricing) ||============================== //
// This is the client-facing "Pricing" page (tenant pricing rules, GET
// /admin/pricing) โ not GET /admin/doormile-pricing, which is Doormile's own
@@ -510,7 +588,7 @@ export const fetchAllRiders = async () => {
const milers = await getMilers();
return { details: milers || [], nextPage: undefined };
} catch (err) {
- console.log('fetchAllRiders err', err.message);
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load riders', 'error', 2000);
return { details: [], nextPage: undefined };
}
};
@@ -519,36 +597,31 @@ export const getallridersummary = async () => {
try {
const milers = (await getMilers()) || [];
// A miler has no `status` field โ availabilitystatus is the real one
- // (confirmed live: Available/Assigned/On_Pickup/Offline). "Active" here
- // means not Offline, matching riders.js's own tab filter.
- const active = milers.filter((m) => String(m.availabilitystatus || '').toLowerCase() !== 'offline').length;
+ // (confirmed live: Available/Assigned/On_Pickup/Offline/Blocked).
+ // "Active" here means not Offline and not Blocked, matching riders.js's
+ // own tab filter.
+ const active = milers.filter((m) => !['offline', 'blocked'].includes(String(m.availabilitystatus || '').toLowerCase())).length;
return { total: milers.length, active, inactive: milers.length - active };
} catch (err) {
- console.log('getallridersummary err', err.message);
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load rider summary', 'error', 2000);
return { total: 0, active: 0, inactive: 0 };
}
};
-// ==============================|| fetchRiders (riders), active riders ||============================== //
-
-export const fetchRiders = async () => {
- try {
- const milers = await getMilers();
- return { details: milers || [], nextPage: undefined };
- } catch (err) {
- console.log('fetchRiders err', err.message);
- return [];
- }
-};
-
-// ==============================|| getriderstatus (riders)||============================== //
-// No check-in-history endpoint in the new API.
-export const getriderstatus = async () => null;
-
// ==============================|| getreportsummary (orders summary)||============================== //
+// appId (queryKey[0]) was previously discarded here โ the LocationAutocomplete
+// zone picker on the Orders Summary page changed the header text but never
+// the actual request. GET /admin/reports does accept a hubid param (see
+// getReports in doormileApi.js) โ hubs are this API's zone-equivalent (see
+// LocationAutocomplete's own comment) โ so pass appId through as that.
export const getreportsummary = async ({ queryKey }) => {
- const [, tenantid, locationid, startdate, enddate] = queryKey;
- return (await getReports(startdate, enddate, tenantid, locationid)) || {};
+ const [appId, tenantid, locationid, startdate, enddate] = queryKey;
+ try {
+ return (await getReports(startdate, enddate, tenantid, locationid, appId || undefined)) || {};
+ } catch (err) {
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load the report summary', 'error', 2000);
+ return {};
+ }
};
// ==============================|| getreportlocationsummary (orders summary)||============================== //
@@ -556,24 +629,14 @@ export const getreportsummary = async ({ queryKey }) => {
// jupiter's getlocationsummary โ GET /admin/locations/summary.
export const getreportlocationsummary = async ({ queryKey }) => {
const [, tenantid, locationid, startdate, enddate] = queryKey;
- return (await getLocationsSummary(tenantid, locationid, startdate, enddate)) || [];
+ try {
+ return (await getLocationsSummary(tenantid, locationid, startdate, enddate)) || [];
+ } catch (err) {
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load the location summary', 'error', 2000);
+ return [];
+ }
};
-// ==============================|| fetchorderdetails (orders detail)||============================== //
-export const fetchorderdetails = async ({ queryKey }) => {
- const [, , , page, rowsPerPage] = queryKey;
- const rows = (await getBookings(page, rowsPerPage)) || [];
- return rows.map((item, index) => ({ ...item, sno: index + 1 }));
-};
-
-// ==============================|| getriderbydelivery (orders detail)||============================== //
-// No per-rider-delivery-trace endpoint in the new API.
-export const getriderbydelivery = async () => [];
-
-// ==============================|| fetchCount (orders detail)||============================== //
-
-export const fetchCount = async () => (await getDashboard()) || {};
-
// ==============================|| fetchRidersSummary (riders summary)||============================== //
// No per-rider performance summary endpoint in the new API.
// Confirmed against jupiter2doormile.md (Status: Done) as the replacement
@@ -582,14 +645,13 @@ export const fetchCount = async () => (await getDashboard()) || {};
// as-is and lets the page's own rendering degrade gracefully on unknown keys.
export const fetchRidersSummary = async ({ queryKey }) => {
const [, appId, startdate, enddate] = queryKey;
- const data = await getMilerSummary(appId || undefined, startdate, enddate);
- return Array.isArray(data) ? data : data ? [data] : [];
-};
-
-// ==============================|| fetchLocations (orders summary))||============================== //
-export const fetchLocations = async () => {
- const partners = (await getPartners()) || [];
- return [...partners, { partnername: 'All', partnerid: -1 }];
+ try {
+ const data = await getMilerSummary(appId || undefined, startdate, enddate);
+ return Array.isArray(data) ? data : data ? [data] : [];
+ } catch (err) {
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load the riders summary', 'error', 2000);
+ return [];
+ }
};
// ==============================|| fetchRidersLogs (RiderLogs)||============================== //
@@ -627,26 +689,21 @@ export const fetchRidersLogs = async ({ queryKey } = {}) => {
latitude: r.currentlatitude,
longitude: r.currentlongitude
}));
- } catch (err) {
- console.log('fetchRidersLogs', err.message);
+ } catch {
+ // Polled every 1s by Dispatch.js's live map (RIDER_LOG_POLL_MS) โ a toast
+ // here would fire once a second on any outage. Fail silently; the map
+ // just stops updating rider pins until the next successful poll.
return [];
}
};
-// ==============================|| getorders (Locations)||============================== //
-
-export const fetchOrders1 = async ({ pageParam = 1, queryKey }) => {
- const [, , , , , , , rowsPerPage] = queryKey;
- const rows = (await getBookings(pageParam, rowsPerPage)) || [];
- return { details: rows, nextPage: rows.length === rowsPerPage ? pageParam + 1 : undefined };
-};
// ==============================|| getusers (viewProfile)||============================== //
export const getusers = async () => {
try {
return (await getProfile()) || {};
} catch (err) {
- console.log('getusers', err.message);
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load profile', 'error', 2000);
return {};
}
};
@@ -656,7 +713,7 @@ export const getallriders = async () => {
try {
return (await getMilers()) || [];
} catch (err) {
- console.log('getallriders', err.message);
+ OpenToast(err.response?.data?.message || err.message || 'Failed to load riders', 'error', 2000);
return [];
}
};
diff --git a/src/pages/api/doormileApi.js b/src/pages/api/doormileApi.js
index 0aefc7a..430a894 100644
--- a/src/pages/api/doormileApi.js
+++ b/src/pages/api/doormileApi.js
@@ -4,7 +4,9 @@ import doormileAxios, { DOORMILE_TOKEN_KEY, DOORMILE_USER_KEY } from 'utils/door
// Mirrors the shape of pages/api/api.js (named async exports, one per endpoint) but talks
// to a different backend with a different envelope: { success, data } / { success, message }.
// See express-console-api.md at the repo root for the full reference this was built from.
-// Not wired into any page yet โ this is plumbing only.
+// Consumed by pages/api/api.js and directly by several pages (createorder1.js,
+// multipleOrders.js, riders.js, editRider.js, Tenants.js, competitiveIntel.js,
+// hubs.js, vehicles.js, login.js).
const buildQuery = (params = {}) => {
const usp = new URLSearchParams();
@@ -516,9 +518,15 @@ export const updateExceptionStatus = async (id, status, resolution) => {
// ==============================|| Competitive intel ||============================== //
-export const getCompetitorBranches = async () => {
- const response = await doormileAxios.get('/admin/competitor-branches');
- return response.data.data;
+// Confirmed live: this resource paginates on page/limit, not the
+// pageno/pagesize convention used everywhere else in the API โ and defaults
+// to only 50 rows (there are 339+ competitor branches on a real tenant), so
+// callers must page through it explicitly rather than trusting one fetch to
+// return everything. Returns the full envelope (data/limit/page/total) so
+// the caller can build pagination controls.
+export const getCompetitorBranches = async (page, limit) => {
+ const response = await doormileAxios.get(`/admin/competitor-branches${buildQuery({ page, limit })}`);
+ return response.data;
};
export const createCompetitorBranch = async (data) => {
@@ -536,9 +544,11 @@ export const deleteCompetitorBranch = async (id) => {
return response.data;
};
-export const getCarrierPricing = async () => {
- const response = await doormileAxios.get('/admin/carrier-pricing');
- return response.data.data;
+// Same page/limit pagination convention as competitor-branches (confirmed
+// live) โ 368+ rows on a real tenant, default page returns only 100.
+export const getCarrierPricing = async (page, limit) => {
+ const response = await doormileAxios.get(`/admin/carrier-pricing${buildQuery({ page, limit })}`);
+ return response.data;
};
export const createCarrierPricing = async (data) => {
diff --git a/src/pages/nearle/clientPricing/clientPricing.js b/src/pages/nearle/clientPricing/clientPricing.js
index 5f0b9db..31edb8f 100644
--- a/src/pages/nearle/clientPricing/clientPricing.js
+++ b/src/pages/nearle/clientPricing/clientPricing.js
@@ -168,7 +168,8 @@ const emptyForm = {
priceperkm: '',
handlingcharges: '',
priority: 1,
- status: 'active'
+ // Confirmed live: matches every real pricing row's casing ("Active").
+ status: 'Active'
};
// ==============================|| Pricing page ||============================== //
@@ -207,24 +208,30 @@ const ClientsPricing = () => {
const zoneName = (row) =>
row.applocation || locationMap.get(row.applocationid)?.locationname || (row.applocationid ? `Zone #${row.applocationid}` : 'โ');
+ // getallpricing() takes no zone arg (see api.js) โ the LocationAutocomplete
+ // only ever changed the header subtitle text, not which rows were shown.
+ // Filter client-side instead; pricing rows already carry applocationid
+ // (used by zoneName above), so this is the same real field, just applied.
+ const zonePricing = useMemo(() => (appId ? pricing.filter((r) => r.applocationid === appId) : pricing), [pricing, appId]);
+
const rows = useMemo(() => {
- if (!debouncedSearch) return pricing;
+ if (!debouncedSearch) return zonePricing;
const q = debouncedSearch.toLowerCase().trim();
- return pricing.filter((row) =>
+ return zonePricing.filter((row) =>
[tenantName(row), row.vehicletype, zoneName(row), String(row.pricingid)]
.filter(Boolean)
.some((field) => String(field).toLowerCase().includes(q))
);
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [pricing, debouncedSearch, tenantMap, locationMap]);
+ }, [zonePricing, debouncedSearch, tenantMap, locationMap]);
const stats = useMemo(() => {
- const total = pricing.length;
- const tenantsPriced = new Set(pricing.map((r) => r.tenantid).filter(Boolean)).size;
- const avgBase = total ? pricing.reduce((sum, r) => sum + (Number(r.baseprice) || 0), 0) / total : 0;
- const activeCount = pricing.filter((r) => String(r.status).toLowerCase() === 'active').length;
+ const total = zonePricing.length;
+ const tenantsPriced = new Set(zonePricing.map((r) => r.tenantid).filter(Boolean)).size;
+ const avgBase = total ? zonePricing.reduce((sum, r) => sum + (Number(r.baseprice) || 0), 0) / total : 0;
+ const activeCount = zonePricing.filter((r) => String(r.status).toLowerCase() === 'active').length;
return { total, tenantsPriced, avgBase, activeCount };
- }, [pricing]);
+ }, [zonePricing]);
const KPI_META = [
{ key: 'total', label: 'Total Pricing Rules', color: BRAND, icon: MdOutlineLocalOffer, value: stats.total },
@@ -305,7 +312,7 @@ const ClientsPricing = () => {
priceperkm: row.priceperkm ?? '',
handlingcharges: row.handlingcharges ?? '',
priority: row.priority ?? 1,
- status: row.status || 'active'
+ status: row.status || 'Active'
});
setDialogOpen(true);
};
@@ -326,6 +333,8 @@ const ClientsPricing = () => {
return;
}
// POST/PUT /admin/pricing body per express-console-api.md.
+ // effectivefrom is required and confirmed live to reject a bare
+ // YYYY-MM-DD date ("invalid request body") โ needs a full ISO datetime.
const data = {
tenantid: form.tenant.tenantid,
applocationid: form.location?.applocationid,
@@ -336,6 +345,7 @@ const ClientsPricing = () => {
basedistance: Number(form.basedistance) || 0,
priceperkm: Number(form.priceperkm),
handlingcharges: Number(form.handlingcharges) || 0,
+ effectivefrom: new Date().toISOString(),
currency: 'INR',
priority: Number(form.priority) || 1,
status: form.status
@@ -765,8 +775,8 @@ const ClientsPricing = () => {
Status
diff --git a/src/pages/nearle/clients/Tenants.js b/src/pages/nearle/clients/Tenants.js
index 2663a8d..a87e570 100644
--- a/src/pages/nearle/clients/Tenants.js
+++ b/src/pages/nearle/clients/Tenants.js
@@ -79,7 +79,7 @@ import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { getalltenants, gettenantsummary } from 'pages/api/api';
+import { getalltenants } from 'pages/api/api';
import {
getAdminPricing,
getDoormilePricing,
@@ -440,7 +440,11 @@ const TenantCustomersDialog = ({ tenant, open, onClose }) => {
id: tenantCustId(row),
firstname: row.firstname || '',
lastname: row.lastname || '',
- phone: row.phone || '',
+ // GET/POST /admin/tenantcustomers has a write/read asymmetry confirmed
+ // live: POST accepts `phone`, but the stored/returned field is
+ // `contactno`. The form field itself stays named `phone` (matching
+ // what create/update accept) โ only the read source differs.
+ phone: row.contactno || '',
email: row.email || ''
});
setFormOpen(true);
@@ -470,7 +474,7 @@ const TenantCustomersDialog = ({ tenant, open, onClose }) => {
{c.firstname} {c.lastname}
- {[c.phone, c.email].filter(Boolean).join(' ยท ')}
+ {[c.contactno, c.email].filter(Boolean).join(' ยท ')}
@@ -580,11 +584,9 @@ const Clients1 = () => {
setPage(0);
};
const handleChange1 = (event, newValue1) => {
- console.log('newValue1', newValue1);
setValue1(newValue1);
};
const handleChange2 = (event, newValue2) => {
- console.log('newValue2', newValue2);
setValue2(newValue2);
};
@@ -600,9 +602,6 @@ const Clients1 = () => {
const handleCollapseToggle2 = (rowIndex) => {
setOpenRowIndex2((prevIndex) => (prevIndex === rowIndex ? null : rowIndex));
};
- useEffect(() => {
- console.log('selectedCustomer', selectedCustomer);
- }, [selectedCustomer]);
useEffect(() => {
setOpenRowIndex1(-1);
setOpenRowIndex2(-1);
@@ -700,15 +699,38 @@ const Clients1 = () => {
queryFn: getalltenants
});
- // ==============================|| gettenantsummary ||============================== //
- const {
- data: summaryData = [],
- isLoading: summaryDataIsLoading,
- refetch: summaryDataRefetch
- } = useQuery({
- queryKey: ['gettenantsummary', appId, status],
- queryFn: gettenantsummary
- });
+ // No tenant-tab-counts endpoint in the new API (gettenantsummary always
+ // returns null) โ derive the Active/Pending/Inactive counts client-side
+ // from the full tenant list instead, which getalltenants already fetches.
+ const tenantStatusCounts = React.useMemo(() => {
+ const counts = { active: 0, pending: 0, inactive: 0 };
+ (tenantList || []).forEach((t) => {
+ const s = String(t.status || '').toLowerCase();
+ if (s === 'active') counts.active += 1;
+ else if (s === 'pending') counts.pending += 1;
+ else if (s === 'inactive') counts.inactive += 1;
+ });
+ return counts;
+ }, [tenantList]);
+
+ // getalltenants() takes no filter args (see api.js) โ the status tabs and
+ // search box only ever changed the query-cache key, never the actual
+ // request, so every tab showed the exact same unfiltered list with just a
+ // relabeled status badge. Filter the already-fetched list client-side
+ // instead, matching the same real `status` field tenantStatusCounts uses.
+ const filteredTenantList = React.useMemo(() => {
+ const activeStatus = STATUS_TABS[value0]?.status;
+ const q = debouncedSearch.trim().toLowerCase();
+ return (tenantList || []).filter((t) => {
+ if (String(t.status || '').toLowerCase() !== activeStatus) return false;
+ if (!q) return true;
+ return (
+ t.tenantname?.toLowerCase().includes(q) ||
+ t.primaryemail?.toLowerCase().includes(q) ||
+ t.primarycontact?.includes(debouncedSearch)
+ );
+ });
+ }, [tenantList, value0, debouncedSearch]);
/* ============================================= || fetchclientpricelist || ============================================= */
@@ -718,7 +740,7 @@ const Clients1 = () => {
const pricing = await getAdminPricing();
setClientpricelist((pricing || []).filter((p) => p.tenantid === selectedTenid));
} catch (err) {
- console.log(err);
+ // swallow
}
};
useEffect(() => {
@@ -731,7 +753,7 @@ const Clients1 = () => {
const pricing = await getAdminPricing();
setTenanatPricing((pricing || []).filter((p) => p.tenantid === id));
} catch (error) {
- console.log('fetchTenanatPricing', error);
+ // swallow
}
};
useEffect(() => {
@@ -760,10 +782,8 @@ const Clients1 = () => {
2000
);
getalltenantsRefetch();
- summaryDataRefetch();
}
} catch (err) {
- console.log(err);
opentoast(err.message, 'error', 1500);
} finally {
setisloader(false);
@@ -776,7 +796,7 @@ const Clients1 = () => {
const pricing = await getDoormilePricing();
setAppPricing((pricing || []).filter((p) => !id || p.applocationid === id));
} catch (error) {
- console.log('appPricingRes', error);
+ // swallow
}
};
/* ============================================= || createpricing || ============================================= */
@@ -793,10 +813,14 @@ const Clients1 = () => {
priceperkg: +selectedPricing.priceperkg || 0,
basedistance: +selectedPricing.basedistance || 0,
handlingcharges: +selectedPricing.handlingcharges || 0,
- effectivefrom: dayjs().format('YYYY-MM-DD'),
+ // Confirmed live: POST /admin/pricing rejects a bare YYYY-MM-DD date
+ // for effectivefrom ("invalid request body") โ needs a full ISO
+ // datetime. Status also confirmed to need title case ("Active"),
+ // matching every real pricing row.
+ effectivefrom: dayjs().toISOString(),
currency: 'INR',
priority: 1,
- status: 'active'
+ status: 'Active'
});
if (res.success) {
enqueueSnackbar('Price Created Successfully', {
@@ -812,7 +836,6 @@ const Clients1 = () => {
opentoast(res.message || 'Failed to save pricing', 'error', 2000);
}
} catch (err) {
- console.log(err);
opentoast(err.response?.data?.message || err.message || 'Failed to save pricing', 'error', 2000);
}
};
@@ -826,9 +849,7 @@ const Clients1 = () => {
opentoast(updateRes.message || 'Update Failed', 'error', 1500);
}
getalltenantsRefetch();
- summaryDataRefetch();
} catch (err) {
- console.log('updateClient', err);
opentoast(err.response?.data?.message || err.message || 'Update Failed', 'error', 1500);
}
};
@@ -837,7 +858,7 @@ const Clients1 = () => {
return (
<>
- {(getalltenantsIsLoading || summaryDataIsLoading || isloader) && }
+ {(getalltenantsIsLoading || isloader) && }
{/* ============================================= || Header | ============================================= */}
{
{KPI_META.map((item, idx) => {
const Icon = item.icon;
- const value = summaryData?.[item.countKey] ?? 0;
+ const value = tenantStatusCounts[item.countKey] ?? 0;
return (
{
value={value ?? 0}
icon={}
color={idx === 0 ? '#662582' : item.color}
- loading={summaryDataIsLoading}
+ loading={getalltenantsIsLoading}
/>
@@ -925,7 +946,7 @@ const Clients1 = () => {
const meta = STATUS_META[t.status];
const Icon = meta.icon;
const active = value0 === idx;
- const count = summaryData?.[t.countKey] ?? 0;
+ const count = tenantStatusCounts[t.countKey] ?? 0;
return (
{
border: 'none'
}}
>
- {summaryDataIsLoading ? : count}
+ {getalltenantsIsLoading ? : count}
);
@@ -1064,14 +1085,14 @@ const Clients1 = () => {
Status
Client
Contact
- Address
+ Delivery OTP
Actions
{getalltenantsIsLoading && }
- {tenantList?.length == 0 && !isloader ? (
+ {filteredTenantList.length == 0 && !isloader ? (
@@ -1088,9 +1109,9 @@ const Clients1 = () => {
) : (
- tenantList?.map((row, index) => {
- const rowStatusKey = (value0 === 0 ? 'active' : value0 === 1 ? 'pending' : 'inactive');
- const rowStatusMeta = STATUS_META[rowStatusKey];
+ filteredTenantList.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => {
+ const rowStatusKey = String(row.status || '').toLowerCase();
+ const rowStatusMeta = STATUS_META[rowStatusKey] || STATUS_META.active;
const RowStatusIcon = rowStatusMeta.icon;
return (
@@ -1162,20 +1183,9 @@ const Clients1 = () => {
-
-
- {row.address || 'โ'}
-
-
+
+ {row.requiredeliveryotp ? 'Yes' : 'No'}
+
@@ -1217,7 +1227,7 @@ const Clients1 = () => {
setDialogopen(true);
setSelectedtenid(row.tenantid);
setAppId(row.applocationid);
- getAppPricing(row.applolcationid);
+ getAppPricing(row.applocationid);
}}
>
@@ -1407,7 +1417,7 @@ const Clients1 = () => {
- {
/>
- {row.firstname}
+ {row.status || 'โ'}
- Contact Person
+ Status
@@ -1461,121 +1471,26 @@ const Clients1 = () => {
-
+ {row.requiredeliveryotp ? (
+
+ ) : (
+
+ )}
- {row.address}
+ {row.requiredeliveryotp ? 'Yes' : 'No'}
- Address
-
-
-
-
-
- {/*
-
-
-
-
-
- {row.subcategoryname}
-
- Category
-
-
-
-
-
-
-
- {row.suburb}
-
-
- Location
-
-
-
- */}
-
-
-
-
-
-
-
- {row.city}
-
- City
-
-
-
-
-
-
-
- {row.postcode}
-
-
- PostCode
-
-
-
-
-
-
-
-
-
-
- {row.latitude}
-
- Latitude
-
-
-
-
-
-
-
- {row.longitude}
-
-
- Longitude
+ Delivery OTP Required
@@ -1681,7 +1596,7 @@ const Clients1 = () => {
flexDirection: 'row'
}}
onClick={() => {
- getAppPricing(selectedCustomer.applolcationid);
+ getAppPricing(selectedCustomer.applocationid);
}}
/>
)}
@@ -2105,7 +2020,7 @@ const Clients1 = () => {
)}
- {tenantList?.length == 0 && !isloader ? (
+ {filteredTenantList.length == 0 && !isloader ? (
@@ -2118,9 +2033,9 @@ const Clients1 = () => {
) : (
- tenantList?.map((row, index) => {
- const rowStatusKey = value0 === 0 ? 'active' : value0 === 1 ? 'pending' : 'inactive';
- const rowStatusMeta = STATUS_META[rowStatusKey];
+ filteredTenantList.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => {
+ const rowStatusKey = String(row.status || '').toLowerCase();
+ const rowStatusMeta = STATUS_META[rowStatusKey] || STATUS_META.active;
const RowStatusIcon = rowStatusMeta.icon;
const expanded = openRowIndex1 === index;
return (
@@ -2199,7 +2114,7 @@ const Clients1 = () => {
setDialogopen(true);
setSelectedtenid(row.tenantid);
setAppId(row.applocationid);
- getAppPricing(row.applolcationid);
+ getAppPricing(row.applocationid);
}}
>
@@ -2280,7 +2195,7 @@ const Clients1 = () => {
setSelectedCustomer(row);
setSelectedtenid(row.tenantid);
setAppId(row.applocationid);
- getAppPricing(row.applolcationid);
+ getAppPricing(row.applocationid);
setDialogopen(true);
}}
>
@@ -2295,21 +2210,13 @@ const Clients1 = () => {
-
-
- {row.address || 'โ'}
-
-
+
{expanded && (
-
-
-
-
-
+
)}
@@ -2321,13 +2228,13 @@ const Clients1 = () => {
)}
{/* ============================================= || Pagination| ============================================= */}
- {!searchword && tenantList?.length > 0 && (
+ {!searchword && filteredTenantList.length > 0 && (
<>
{
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
+ const navigate = useNavigate();
+
+ const loggedInTenantId = localStorage.getItem('tenantid');
+ const isStaffLogin = !loggedInTenantId || loggedInTenantId === '0';
+
+ const [appId, setAppId] = useState(0);
+ const [locaName, setLocoName] = useState('Select Location');
+ const [tid, setTid] = useState(isStaffLogin ? '' : loggedInTenantId);
+
+ const [firstname, setFirstname] = useState('');
+ const [lastname, setLastname] = useState('');
+ const [phone, setPhone] = useState('');
+ const [email, setEmail] = useState('');
+ const [doorno, setDoorno] = useState('');
+ const [inputValue2, setInputValue2] = useState('');
+ const [landmark, setLandmark] = useState('');
+ const [pickCust, setPickCust] = useState({});
+ const [startPoint, setStartPoint] = useState({ latitude: 0, longitude: 0 });
+ const [appLocaLat, setAppLocaLat] = useState();
+ const [appLocaLng, setAppLocaLng] = useState();
+
+ const { data: tenantList = [], isLoading: tenantListLoading } = useQuery({
+ queryKey: ['admin-tenants-picker'],
+ queryFn: getAdminTenants,
+ enabled: isStaffLogin
+ });
+
+ // LocationAutocomplete only reports back applocationid/locationname โ it
+ // caches the full hub list (with lat/lng) in localStorage('applocations')
+ // as a side effect, so pull the coordinates for the address-search bias
+ // from there rather than duplicating the GET /admin/hubs call.
+ useEffect(() => {
+ if (!appId) return;
+ try {
+ const hubs = JSON.parse(localStorage.getItem('applocations') || '[]');
+ const hub = hubs.find((h) => h.applocationid === appId);
+ if (hub?.latitude) {
+ setAppLocaLat(hub.latitude);
+ setAppLocaLng(hub.longitude);
+ }
+ } catch (err) {
+ // bias is a soft nicety โ ignore a bad cache
+ }
+ }, [appId]);
+
+ const handlePlaceSelected = (place) => {
+ setStartPoint({ latitude: place.geometry.location.lat(), longitude: place.geometry.location.lng() });
+ const parsed = { suburb: '', city: '', state: '', postcode: '' };
+ place.address_components.forEach((component) => {
+ component.types.forEach((type) => {
+ switch (type) {
+ case 'sublocality_level_1':
+ case 'sublocality':
+ parsed.suburb = component.long_name;
+ break;
+ case 'locality':
+ parsed.city = component.long_name;
+ break;
+ case 'administrative_area_level_1':
+ parsed.state = component.long_name;
+ break;
+ case 'postal_code':
+ parsed.postcode = component.long_name;
+ break;
+ }
+ });
+ });
+ setPickCust({
+ address: place.formatted_address,
+ suburb: parsed.suburb,
+ city: parsed.city,
+ state: parsed.state,
+ postcode: parsed.postcode,
+ latitude: place.geometry.location.lat(),
+ longitude: place.geometry.location.lng()
+ });
+ };
+
+ const opentoast = (message) => {
+ enqueueSnackbar(message, {
+ variant: 'error',
+ anchorOrigin: { vertical: 'top', horizontal: 'right' },
+ autoHideDuration: 2000
+ });
+ };
+
+ const createCustomerMutation = useMutation({
+ mutationFn: (payload) => createTenantCustomer(payload),
+ onSuccess: (res) => {
+ if (res.success) {
+ enqueueSnackbar('Customer created successfully', {
+ variant: 'success',
+ anchorOrigin: { vertical: 'top', horizontal: 'right' },
+ autoHideDuration: 2000
+ });
+ navigate('/doormile/customers');
+ } else {
+ opentoast(res.message || 'Could not create customer');
+ }
+ },
+ onError: (err) => {
+ opentoast(err.response?.data?.message || err.message || 'Failed to create customer');
+ }
+ });
+
+ const createprofile = () => {
+ if (isStaffLogin && !tid) {
+ opentoast('Select Tenant');
+ } else if (!firstname) {
+ opentoast('Enter Name');
+ } else if (!phone || phone.length !== 10) {
+ opentoast('Enter a valid 10-digit Mobile Number');
+ } else {
+ createCustomerMutation.mutate({
+ firstname,
+ lastname,
+ phone,
+ email,
+ ...(isStaffLogin && tid ? { tenantid: +tid } : {})
+ });
+ }
+ };
+
+ return (
+ <>
+ {createCustomerMutation.isPending && }
+
+
+
+
+
+
+
+ Create Customer
+
+
+
+
+
+
+
+
+ {/* ===================================================== || Location & Client || ===================================================== */}
+
+
+ Location
+
+
+
+ {isStaffLogin && (
+
+
+ Choose Client
+ option?.tenantname || ''}
+ renderInput={(params) => }
+ onChange={(e, val) => setTid(val ? val.tenantid : '')}
+ />
+
+
+ )}
+
+ {/* ===================================================== || Name / Phone / Email || ===================================================== */}
+
+
+ Name
+ setFirstname(e.target.value)}
+ value={firstname}
+ autoComplete="off"
+ />
+
+
+
+
+ Last Name
+ setLastname(e.target.value)}
+ value={lastname}
+ autoComplete="off"
+ />
+
+
+
+
+ Phone Number
+
+
+ {
+ if (e.target.value.toString().length <= 10) {
+ setPhone(e.target.value);
+ }
+ }}
+ value={phone}
+ autoComplete="off"
+ />
+
+
+
+
+
+ Email
+ setEmail(e.target.value)}
+ value={email}
+ autoComplete="off"
+ />
+
+
+
+ {/* ===================================================== || Door No || ===================================================== */}
+
+
+ Door No
+ setDoorno(e.target.value)}
+ value={doorno}
+ autoComplete="off"
+ />
+
+
+
+ {/* ===================================================== || Address section โ collected, not persisted || ===================================================== */}
+
+
+ The backend doesn't store an address against a customer yet โ these fields aren't required and won't be saved on submit.
+
+
+
+
+ Address
+ setInputValue2(text)}
+ onPlaceSelected={handlePlaceSelected}
+ bias={{ lat: appLocaLat, lng: appLocaLng }}
+ TextFieldProps={{
+ variant: 'outlined',
+ InputProps: {
+ endAdornment: (
+ {
+ setInputValue2('');
+ setPickCust({});
+ setStartPoint({ latitude: 0, longitude: 0 });
+ }}
+ size="small"
+ >
+
+
+ )
+ }
+ }}
+ />
+
+
+
+
+ Location
+ setPickCust((p) => ({ ...p, suburb: e.target.value }))}
+ value={pickCust.suburb || ''}
+ autoComplete="off"
+ />
+
+
+
+
+ City
+ setPickCust((p) => ({ ...p, city: e.target.value }))}
+ value={pickCust.city || ''}
+ autoComplete="off"
+ />
+
+
+
+
+ State
+ setPickCust((p) => ({ ...p, state: e.target.value }))}
+ value={pickCust.state || ''}
+ autoComplete="off"
+ />
+
+
+
+
+ Post Code
+ setPickCust((p) => ({ ...p, postcode: e.target.value }))}
+ value={pickCust.postcode || ''}
+ autoComplete="off"
+ />
+
+
+
+
+ Landmark
+ setLandmark(e.target.value)} value={landmark} autoComplete="off" />
+
+
+
+
+ Latitude
+
+
+
+
+
+ Longitude
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default Createcustomer;
diff --git a/src/pages/nearle/clients/createclient.js b/src/pages/nearle/clients/createclient.js
index 87ae28a..8cf8186 100644
--- a/src/pages/nearle/clients/createclient.js
+++ b/src/pages/nearle/clients/createclient.js
@@ -36,7 +36,11 @@ const Createclient = () => {
const [tenantname, setTenantname] = useState('');
const [primaryemail, setPrimaryemail] = useState('');
const [primarycontact, setPrimarycontact] = useState('');
- const [status, setStatus] = useState('active');
+ // Confirmed live: POST /admin/tenants doesn't reject lowercase status, but
+ // stores it exactly as sent โ every real tenant uses "Active" (title
+ // case), so a lowercase one would be inconsistent with the rest of the
+ // data even though the request itself succeeds.
+ const [status, setStatus] = useState('Active');
const [requiredeliveryotp, setRequiredeliveryotp] = useState(false);
const opentoast = (message) => {
@@ -137,8 +141,8 @@ const Createclient = () => {
Status
diff --git a/src/pages/nearle/competitiveIntel/competitiveIntel.js b/src/pages/nearle/competitiveIntel/competitiveIntel.js
index 0b0d5f9..2cd1a59 100644
--- a/src/pages/nearle/competitiveIntel/competitiveIntel.js
+++ b/src/pages/nearle/competitiveIntel/competitiveIntel.js
@@ -10,6 +10,7 @@ import {
Grid,
IconButton,
InputLabel,
+ Pagination,
Paper,
Stack,
Table,
@@ -40,13 +41,21 @@ import {
import { enqueueSnackbar } from 'notistack';
// ============================================================================
-// Neither /admin/competitor-branches nor /admin/carrier-pricing document a
-// request/response body anywhere โ express-console-api.md lists only the
-// routes. The fields below (branchname/city/address for branches,
-// carriername/vehicletype/baseprice/priceperkm for pricing) are a best-effort
-// guess following the naming conventions used elsewhere in this API and are
-// UNVERIFIED โ expect the create/edit calls here to need correction once a
-// real request/response has been seen.
+// Field names confirmed live (create+verify+delete round-trip against
+// production, per an explicit one-off testing pass) โ the original guesses
+// here (branchname/competitorname/city for branches, carriername/
+// vehicletype/baseprice/priceperkm for pricing) were entirely wrong and have
+// been replaced with the real shape:
+//
+// competitor-branches: { id, company, area, phone, plus_code, address,
+// pincodes, rate_per_kg, offers_pickup, offers_drop, time_in_days,
+// packing_charge, frequency, created_at, updated_at }
+// carrier-pricing: { id, company, weight_slab, service_type, zone, rate,
+// delivery_time, created_at, updated_at }
+//
+// rate_per_kg/rate are free-text strings in the real data (e.g. "600",
+// "25-35", "Not Answered", ".") โ not clean numbers โ so they're kept as
+// plain text fields, not Number-cast.
// ============================================================================
const DT = {
@@ -67,24 +76,50 @@ const TABS = [
{ key: 'pricing', label: 'Carrier Pricing' }
];
-const emptyBranch = { id: null, branchname: '', competitorname: '', city: '', address: '' };
-const emptyCarrier = { id: null, carriername: '', vehicletype: '', baseprice: '', priceperkm: '' };
+const emptyBranch = {
+ id: null,
+ company: '',
+ area: '',
+ phone: '',
+ address: '',
+ pincodes: '',
+ rate_per_kg: '',
+ offers_pickup: '',
+ offers_drop: '',
+ time_in_days: '',
+ packing_charge: ''
+};
+const emptyCarrier = { id: null, company: '', weight_slab: '', service_type: '', zone: '', rate: '', delivery_time: '' };
+const PAGE_SIZE = 50;
const CompetitiveIntel = () => {
const queryClient = useQueryClient();
const [tab, setTab] = useState('branches');
+ const [page, setPage] = useState(1);
const [dialogOpen, setDialogOpen] = useState(false);
const [branchForm, setBranchForm] = useState(emptyBranch);
const [carrierForm, setCarrierForm] = useState(emptyCarrier);
- const { data: branches = [], isLoading: branchesLoading } = useQuery({
- queryKey: ['competitor-branches'],
- queryFn: getCompetitorBranches
+ const changeTab = (t) => {
+ setTab(t);
+ setPage(1);
+ };
+
+ const { data: branchesRes, isLoading: branchesLoading } = useQuery({
+ queryKey: ['competitor-branches', page],
+ queryFn: () => getCompetitorBranches(page, PAGE_SIZE),
+ enabled: tab === 'branches',
+ keepPreviousData: true
});
- const { data: carrierPricing = [], isLoading: carrierLoading } = useQuery({
- queryKey: ['carrier-pricing'],
- queryFn: getCarrierPricing
+ const branches = branchesRes?.data || [];
+
+ const { data: carrierRes, isLoading: carrierLoading } = useQuery({
+ queryKey: ['carrier-pricing', page],
+ queryFn: () => getCarrierPricing(page, PAGE_SIZE),
+ enabled: tab === 'pricing',
+ keepPreviousData: true
});
+ const carrierPricing = carrierRes?.data || [];
const opentoast = (message, variant = 'error') =>
enqueueSnackbar(message, { variant, anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 });
@@ -139,8 +174,6 @@ const CompetitiveIntel = () => {
onError: (err) => opentoast(err.response?.data?.message || err.message || 'Failed to delete carrier pricing')
});
- const idOf = (row) => row.id ?? row.competitorbranchid ?? row.carrierpricingid;
-
const openCreate = () => {
if (tab === 'branches') setBranchForm(emptyBranch);
else setCarrierForm(emptyCarrier);
@@ -150,19 +183,27 @@ const CompetitiveIntel = () => {
const openEdit = (row) => {
if (tab === 'branches') {
setBranchForm({
- id: idOf(row),
- branchname: row.branchname || '',
- competitorname: row.competitorname || '',
- city: row.city || '',
- address: row.address || ''
+ id: row.id,
+ company: row.company || '',
+ area: row.area || '',
+ phone: row.phone || '',
+ address: row.address || '',
+ pincodes: row.pincodes || '',
+ rate_per_kg: row.rate_per_kg || '',
+ offers_pickup: row.offers_pickup || '',
+ offers_drop: row.offers_drop || '',
+ time_in_days: row.time_in_days || '',
+ packing_charge: row.packing_charge || ''
});
} else {
setCarrierForm({
- id: idOf(row),
- carriername: row.carriername || '',
- vehicletype: row.vehicletype || '',
- baseprice: row.baseprice ?? '',
- priceperkm: row.priceperkm ?? ''
+ id: row.id,
+ company: row.company || '',
+ weight_slab: row.weight_slab || '',
+ service_type: row.service_type || '',
+ zone: row.zone || '',
+ rate: row.rate || '',
+ delivery_time: row.delivery_time || ''
});
}
setDialogOpen(true);
@@ -170,30 +211,32 @@ const CompetitiveIntel = () => {
const handleDelete = (row) => {
if (!window.confirm('Delete this record?')) return;
- if (tab === 'branches') branchDeleteMutation.mutate(idOf(row));
- else carrierDeleteMutation.mutate(idOf(row));
+ if (tab === 'branches') branchDeleteMutation.mutate(row.id);
+ else carrierDeleteMutation.mutate(row.id);
};
const handleSave = () => {
if (tab === 'branches') {
- if (!branchForm.branchname) {
- opentoast('Fill Branch Name');
+ if (!branchForm.company) {
+ opentoast('Fill Company');
return;
}
const { id, ...data } = branchForm;
branchMutation.mutate({ id, data });
} else {
- if (!carrierForm.carriername || !carrierForm.baseprice) {
- opentoast('Fill Carrier Name and Base Price');
+ if (!carrierForm.company || !carrierForm.weight_slab) {
+ opentoast('Fill Company and Weight Slab');
return;
}
const { id, ...data } = carrierForm;
- carrierMutation.mutate({ id, data: { ...data, baseprice: Number(data.baseprice) || 0, priceperkm: Number(data.priceperkm) || 0 } });
+ carrierMutation.mutate({ id, data });
}
};
const rows = tab === 'branches' ? branches : carrierPricing;
const isLoading = tab === 'branches' ? branchesLoading : carrierLoading;
+ const total = (tab === 'branches' ? branchesRes?.total : carrierRes?.total) || 0;
+ const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
return (
<>
@@ -214,7 +257,7 @@ const CompetitiveIntel = () => {
{TABS.map((t) => (
setTab(t.key)}
+ onClick={() => changeTab(t.key)}
sx={{
px: 2,
py: 0.75,
@@ -232,26 +275,56 @@ const CompetitiveIntel = () => {
))}
-
-
-
+
+ {total} total ยท page {page} of {pageCount}
+
+
+
+
+
{tab === 'branches' ? (
-
+
#
- Branch Name
- Competitor
- City
+ Company
+ Area
+ Phone
Address
+ Rate/Kg
Actions
) : (
-
+
#
- Carrier
- Vehicle Type
- Base Price
- Price/Km
+ Company
+ Weight Slab
+ Zone
+ Rate
Actions
)}
@@ -259,7 +332,7 @@ const CompetitiveIntel = () => {
{rows.length === 0 && !isLoading ? (
-
+
@@ -272,21 +345,29 @@ const CompetitiveIntel = () => {
) : (
rows.map((row, index) => (
-
- {index + 1}
+
+ {(page - 1) * PAGE_SIZE + index + 1}
{tab === 'branches' ? (
<>
- {row.branchname || 'โ'}
- {row.competitorname || 'โ'}
- {row.city || 'โ'}
- {row.address || 'โ'}
+ {row.company || 'โ'}
+ {row.area || 'โ'}
+ {row.phone || 'โ'}
+
+
+ {row.address || 'โ'}
+
+
+ {row.rate_per_kg || 'โ'}
>
) : (
<>
- {row.carriername || 'โ'}
- {row.vehicletype || 'โ'}
- {row.baseprice ?? 'โ'}
- {row.priceperkm ?? 'โ'}
+ {row.company || 'โ'}
+ {row.weight_slab || 'โ'}
+ {row.zone || 'โ'}
+ {row.rate || 'โ'}
>
)}
@@ -311,62 +392,149 @@ const CompetitiveIntel = () => {
-
@@ -267,7 +316,7 @@ const Customers = () => {
{isLoading && }
{rows.length === 0 && !isLoading ? (
-
+
@@ -284,25 +333,42 @@ const Customers = () => {
key={custId(row) || index}
sx={{ '& td': { borderBottom: `1px solid ${DT.divider}` }, '&:hover': { backgroundColor: DT.surfaceAlt } }}
>
- {index + 1}
+
+
+ {String(index + 1).padStart(2, '0')}
+
+
-
- {custName(row)}
-
+
+
+ {custName(row)}
+
+
+ ID #{custId(row) ?? 'โ'}
+
+
-
- {custPhone(row)}
-
-
-
-
- {custEmail(row)}
+
+
+
+
+ {custPhone(row)}
+
+
+ {row.email && (
+
+
+
+ {row.email}
+
+
+ )}
{custBookings(row)}
@@ -322,31 +388,69 @@ const Customers = () => {
)}
- setEditRow(null)} maxWidth="xs" fullWidth>
- Edit Customer
+ setEditRow(null)} maxWidth="xs" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}>
+
+
+
+
+
+
+
+ Customer
+
+
+ Edit {editRow ? custName(editRow) : 'Customer'}
+
+
+
+
-
+
Name
- setForm((f) => ({ ...f, name: e.target.value }))} />
+ setForm((f) => ({ ...f, name: e.target.value }))} />
- Phone
- setForm((f) => ({ ...f, phone: e.target.value }))}
- InputProps={{ startAdornment: }}
- />
+ Phone Number
+
+
+ setForm((f) => ({ ...f, phone: e.target.value.replace(/\D/g, '') }))}
+ InputProps={{ startAdornment: }}
+ />
+
Email
- setForm((f) => ({ ...f, email: e.target.value }))} />
+ setForm((f) => ({ ...f, email: e.target.value }))}
+ InputProps={{ startAdornment: }}
+ />
-
-
diff --git a/src/pages/nearle/deliveries/deliveries.js b/src/pages/nearle/deliveries/deliveries.js
index 41ace71..1812722 100644
--- a/src/pages/nearle/deliveries/deliveries.js
+++ b/src/pages/nearle/deliveries/deliveries.js
@@ -329,7 +329,6 @@ const Deliveries = () => {
const handleMenuOpen = (event, row) => {
setSelectedRow(row);
- console.log('selectedRow', row);
setMenuAnchorEl(event.currentTarget);
};
@@ -346,7 +345,6 @@ const Deliveries = () => {
opentoast('Delivery Cancelled Successfully', 'success');
setCancelDeliveryOpen(false);
fetchCountRefetch(); // Refresh count data
- fetchDeliveriesRefetch(); // Refresh deliveries
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
},
onError: (error) => {
@@ -357,14 +355,12 @@ const Deliveries = () => {
// ==============================|| cancelridernotification ||============================== //
const cancelridernotification = async () => {
- console.log('cancelridernotification', selectedRow);
try {
// /admin/milers/:id/notify keys off milerprofileid, not the userid
// stored on the booking (confirmed live) โ see fetchDeliveries in api.js.
return await notifyMiler(selectedRow.milerprofileid, 'DoormileXpress', `${selectedRow.orderid} have been Cancelled`);
} catch (err) {
opentoast(err.message, 'error', 2000);
- console.log(err);
}
};
@@ -425,7 +421,6 @@ const Deliveries = () => {
opentoast('Rider Changed Successfully', 'success');
}
fetchCountRefetch(); // Refresh count data
- fetchDeliveriesRefetch(); // Refresh deliveries
countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips
// notifyMiler wants a miler profile id, not an FCM token โ the new API
// looks the device token up server-side. selectedRider comes straight
@@ -507,7 +502,6 @@ const Deliveries = () => {
setCurrentStatus('cancelled');
setTotalCount(countData?.cancelLength);
}
- console.log(i);
setSearchword('');
};
@@ -515,31 +509,6 @@ const Deliveries = () => {
setOpen(false);
};
- /* ============================================= || fetchDeliveries | ============================================= */
-
- const {
- isLoading: fetchDeliveriesIsLoading,
- isError: fetchDeliveriesIsError,
- error: fetchDeliveriesError,
- refetch: fetchDeliveriesRefetch
- } = useInfiniteQuery({
- queryKey: [
- 'fetchdeliveries',
- appId,
- userid,
- currentStatus,
- startdate,
- enddate,
- rowsPerPage,
- debouncedSearch,
- tenantid,
- locationid,
- riderid
- ],
- queryFn: fetchDeliveries,
- getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined
- });
-
// (filteredRows is defined below, after countSourceRows / countSourceLoading
// are in scope โ they're the single source of truth for both the table and
// the per-status chip counts.)
@@ -740,7 +709,6 @@ const Deliveries = () => {
queryFn: () => fetchCountAPI(appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid)
});
useEffect(() => {
- console.log('countData', countData);
if (tabvalue === 0 && countData) {
setTotalCount(countData.uncoveredLength);
}
@@ -755,11 +723,11 @@ const Deliveries = () => {
error: ridersListError
} = useQuery({
queryKey: ['ridersList', appId], // Unique key for caching & re-fetching
+ // fetchRidersList now toasts + returns [] internally on failure (was
+ // throwing after already toasting there too, which showed this same
+ // error twice) โ no onError needed here anymore.
queryFn: fetchRidersList,
- enabled: Boolean(appId),
- onError: (err) => {
- OpenToast(err.message, 'error', 2000);
- }
+ enabled: Boolean(appId)
});
/* ============================================= || updatedelivery | ============================================= */
@@ -767,26 +735,21 @@ const Deliveries = () => {
const updateDeliveryMutation = useMutation({
mutationFn: (orderData) => updateDeliveryAPI(orderData),
onSuccess: (res) => {
- console.log(res);
if (res.success) {
opentoast('Updated Successfully', 'success');
setDeliveryamount('');
setNotes('');
setDialogopen(false);
- fetchDeliveriesRefetch();
fetchCountRefetch();
countSourceRefetch();
}
},
onError: (err) => {
- console.log(err);
opentoast(err.message, 'success');
}
});
- const errorMessage = fetchDeliveriesIsError
- ? `Error fetching deliveries: ${fetchDeliveriesError?.message}`
- : fetchCountIsError
+ const errorMessage = fetchCountIsError
? `Error fetching count summary: ${fetchCountError?.message}`
: ridersListIsError
? `Error fetching riders: ${ridersListError?.message}`
@@ -797,7 +760,6 @@ const Deliveries = () => {
: null;
if (errorMessage) {
- console.log('errorMessage', errorMessage);
OpenToast(errorMessage, 'error', 2000);
return null; // or return <>> if inside a component
}
@@ -816,7 +778,7 @@ const Deliveries = () => {
color: '#fff',
zIndex: (theme) => theme.zIndex.drawer + 1
}}
- open={fetchCountIsLoading || fetchDeliveriesIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading}
+ open={fetchCountIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading}
>
{/* */}
@@ -1736,8 +1698,8 @@ const Deliveries = () => {
{showAction && Action}
- {(loading1 || countSourceIsLoading) && }
+ {(loading1 || countSourceIsLoading) && }
{filteredRows.length == 0 && !loading1 && !countSourceLoading && (
@@ -1816,7 +1778,6 @@ const Deliveries = () => {
setDeliverylist([...arr]);
}
}
- console.log('list', deliverylist);
}}
checked={deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)}
/>
@@ -2696,7 +2657,6 @@ const Deliveries = () => {
setDatestatus('');
}
}
- console.log(range);
}}
definedRanges={[
{
diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css
index e254f05..fcb8486 100644
--- a/src/pages/nearle/dispatch/Dispatch.css
+++ b/src/pages/nearle/dispatch/Dispatch.css
@@ -17,8 +17,18 @@
.dispatch-container {
width: calc(100% + 48px);
- height: calc(100vh - 88px);
- margin: -24px;
+ /* MainLayout/index.js's .main-content-area has NO padding-top (only
+ left/right/bottom at 24px) โ Astryx's Layout.tsx renders the TopNav as
+ a normal-flow flex child ahead of the content area, so content already
+ starts right below it with no extra offset needed. This page fills the
+ screen by negative-margining the 24px padding away entirely and sizing
+ to the viewport height minus the TopNav's own (dynamically measured)
+ height. An earlier version of this rule compensated for a padding-top
+ that main-content-area briefly had and no longer does โ if that
+ padding-top ever comes back, this needs to account for it again (see
+ git history around this comment for that version's math). */
+ height: calc(100vh - var(--appshell-header-height, 64px));
+ margin: 0 -24px -24px -24px;
display: flex;
flex-direction: column;
background: var(--bg);
@@ -10982,7 +10992,7 @@
its stacked children instead of being clipped to a single row height. */
.dispatch-container {
height: auto;
- min-height: calc(100vh - 88px);
+ min-height: calc(100vh - var(--appshell-header-height, 64px));
overflow-y: auto;
overflow-x: hidden;
}
diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js
index 621baa2..c2bfe08 100644
--- a/src/pages/nearle/dispatch/Dispatch.js
+++ b/src/pages/nearle/dispatch/Dispatch.js
@@ -1314,7 +1314,16 @@ const Dispatch = ({
const { data: riderInfoData, isFetching: riderInfoFetching, isError: riderInfoIsError, error: riderInfoError } = useQuery({
queryKey: ['riderPeriodicLog', riderInfoUserid],
- queryFn: () => getRiderPeriodicLogs(riderInfoUserid),
+ // riderInfoUserid is actually a userid (it's sourced from ridersAllDay,
+ // which comes from booking data โ assignedmileruserid, confirmed live)
+ // but /admin/milers/:id/* routes need milerprofileid. liveRiderLocations
+ // (declared below) carries both, so resolve the real id here rather than
+ // changing riderInfoUserid itself, which is compared against elsewhere
+ // in this file for sidebar row highlighting in userid-space.
+ queryFn: () => {
+ const match = liveRiderLocations.find((r) => String(r.userid) === String(riderInfoUserid));
+ return getRiderPeriodicLogs(match?.milerprofileid ?? riderInfoUserid);
+ },
enabled: viewMode === 'rider-info' && riderInfoUserid != null,
// Auto-refresh the rider snapshot every 15s while the view is open and a
// rider is selected. Don't poll while the tab is hidden so we don't burn
@@ -1544,8 +1553,14 @@ const Dispatch = ({
const lon = parseFloat(r?.longitude);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
return {
+ // id stays userid-keyed โ it's what joins this live-GPS feed against
+ // `riders`/orders elsewhere in this file, which are themselves keyed
+ // by a booking's assignedmileruserid (also a userid, confirmed live).
+ // milerprofileid is kept separately for the one place that actually
+ // needs it: /admin/milers/:id/* calls (the rider-info panel).
id: String(r.userid ?? ''),
userid: r.userid,
+ milerprofileid: r.milerprofileid,
username: r.username || `Rider #${r.userid}`,
status: String(r.status || '').toLowerCase(),
contactno: r.contactno,
@@ -1558,12 +1573,15 @@ const Dispatch = ({
.filter(Boolean);
}, [ridersLocationLogs]);
- // Set of rider ids whose latest GPS log row is `active` (i.e. on the road
+ // Set of rider ids whose latest position is `active` (i.e. on the road
// right now). The "All Active Routes" view (viewMode === 'all') uses this to
// show ONLY currently-active riders โ their cards, routes, drop markers and
// live bike markers โ and hide everyone who is offline/idle for the slot.
+ // GET /admin/milers/summary's availabilitystatus enum is confirmed live as
+ // Available/Assigned/On_Pickup/Offline โ not active/pending, which never
+ // matched anything and left this set permanently empty.
const activeRiderIdSet = useMemo(
- () => new Set(liveRiderLocations.filter((r) => r.status === 'active' || r.status === 'pending').map((r) => String(r.id))),
+ () => new Set(liveRiderLocations.filter((r) => r.status !== 'offline' && r.status !== 'blocked').map((r) => String(r.id))),
[liveRiderLocations]
);
// Default to the slot containing the current wall-clock time. Use a
@@ -1707,6 +1725,12 @@ const Dispatch = ({
const riderDailyData = {};
liveRows.forEach(r => {
+ // Cancelled/skipped orders never delivered, so they shouldn't earn
+ // revenue or be charged fuel/slot cost either โ matches the same fix
+ // already applied to ProfitabilitySection.js's calcRiderMetrics.
+ const status = String(r.orderstatus ?? r.status ?? '').toLowerCase();
+ if (SKIPPED_STATUSES.has(status)) return;
+
// Filter by selectedDate to align with daily slot aggregations
const dateStr = r.assigntime
? dayjs(r.assigntime).format('YYYY-MM-DD')
@@ -2053,7 +2077,7 @@ const Dispatch = ({
() =>
isAllActiveView
? liveRiderLocations
- .filter((r) => (r.status === 'active' || r.status === 'pending') && activeOrderRiderIdSet.has(String(r.id)))
+ .filter((r) => r.status !== 'offline' && r.status !== 'blocked' && activeOrderRiderIdSet.has(String(r.id)))
.map((r) => [r.lat, r.lon])
: [],
[isAllActiveView, liveRiderLocations, activeOrderRiderIdSet]
@@ -5129,7 +5153,8 @@ const Dispatch = ({
{/* Live rider GPS markers from GET /admin/milers/summary (see
fetchRidersLogs in api.js). Green pin when the rider's status
- is `active`, red otherwise, with the rider's username as a
+ isn't `offline` (Available/Assigned/On_Pickup โ confirmed
+ live enum), red otherwise, with the rider's username as a
label. Scoped to riders who actually have orders in the
currently selected slot โ `riders` is derived from
filteredLiveRows so it already reflects the slot filter. A
@@ -5139,12 +5164,12 @@ const Dispatch = ({
{liveRiderLocations
.filter((r) =>
isAllActiveView
- ? ((r.status === 'active' || r.status === 'pending') && activeOrderRiderIdSet.has(String(r.id)))
+ ? (r.status !== 'offline' && r.status !== 'blocked' && activeOrderRiderIdSet.has(String(r.id)))
: riders.some((rd) => String(rd.id) === String(r.id))
)
.filter((r) => !focusedRider || String(focusedRider.id) === String(r.id))
.map((r) => {
- const isActive = r.status === 'active' || r.status === 'pending';
+ const isActive = r.status !== 'offline' && r.status !== 'blocked';
const pinColor = isActive ? '#16a34a' : '#dc2626';
// Look up the rider's in-progress order so the popup can show
// where they're heading next (drop customer/area + originating
diff --git a/src/pages/nearle/dispatch/Preview.js b/src/pages/nearle/dispatch/Preview.js
index 6681a90..52d8575 100644
--- a/src/pages/nearle/dispatch/Preview.js
+++ b/src/pages/nearle/dispatch/Preview.js
@@ -328,7 +328,6 @@ const Preview = () => {
const deliveryData = stateData.deliveryData || [];
const autoRiders = stateData.autoRiders || [];
const absentRidersPayload = stateData.absentRidersPayload || [];
- const rider = stateData.rider || null;
const appId = useMemo(() => {
if (stateData.appId) return stateData.appId;
@@ -409,11 +408,34 @@ const Preview = () => {
});
const createFinalDeliveryMutation = useMutation({
+ // finalCreatedeliveries now assigns each order directly on Doormile's
+ // own booking record (POST /admin/bookings/:id/assign-miler per order,
+ // resolved internally) instead of posting to jupiter โ see api.js for
+ // why: jupiter's createdeliveries wrote into jupiter's own orphaned
+ // database, which neither the Orders "pending" list nor the Deliveries
+ // "dispatched" filter ever read (both come from GET /admin/bookings).
mutationFn: finalCreatedeliveries,
onSuccess: () => {
OpenToast('Delivery Created Successfully', 'success', 2000);
setIsLoading(false);
- if (rider?.userfcmtoken) notifyRiderMutation.mutate(rider.userfcmtoken);
+ // stateData.rider (a single rider forwarded via navigate() from the
+ // Orders page) is never actually populated in the real flow โ that
+ // page's navigate() call doesn't include a `rider` key at all โ so
+ // this was a permanent no-op and no rider ever got notified after
+ // assignment. Notify every rider actually present in the committed
+ // list instead. notifyRider expects a milerprofileid (not an FCM
+ // token โ the server looks the device up itself); rider_id/userid
+ // here is the id this page already treats as canonical throughout
+ // (see flattenRiders/moveOrderInPreviewData above) since it's the
+ // only rider identifier the solver echoes back โ unconfirmed whether
+ // that's actually a milerprofileid by the time it reaches here.
+ const notifiedRiderIds = new Set();
+ finaldeliveryList.forEach((order) => {
+ const riderId = order.rider_id ?? order.userid;
+ if (riderId == null || notifiedRiderIds.has(String(riderId))) return;
+ notifiedRiderIds.add(String(riderId));
+ notifyRiderMutation.mutate(riderId);
+ });
navigate('/doormile/deliveries');
},
onError: (error) => {
@@ -479,6 +501,18 @@ const Preview = () => {
OpenToast('No deliveries to assign', 'error', 3000);
return;
}
+ // "Change Rider" and "Reconcile" were two independent buttons with
+ // nothing linking them โ an operator could edit a rider's steps and hit
+ // Assign Orders without ever pressing Reconcile. Committing stale step
+ // ordering corrupts route sequences server-side (see this folder's
+ // CLAUDE.md ยง3, called out as the single biggest production risk here).
+ // dirtyRiderIds only ever holds riders edited since the last successful
+ // reconcile of that specific rider, so size > 0 means real unreconciled
+ // edits are pending.
+ if (dirtyRiderIds.size > 0) {
+ OpenToast(`Reconcile ${dirtyRiderIds.size} edited rider(s) before assigning`, 'warning', 4000);
+ return;
+ }
setIsLoading(true);
createFinalDeliveryMutation.mutate({ deliveries: finaldeliveryList });
};
@@ -789,9 +823,18 @@ const Preview = () => {
>
Back
-
- Assign Orders
-
+ 0 ? `Reconcile ${dirtyRiderIds.size} edited rider(s) first` : ''}>
+
+ 0}
+ onClick={handleFinalCreateDelivery}
+ >
+ Assign Orders
+
+
+
diff --git a/src/pages/nearle/dispatch/ProfitabilitySection.js b/src/pages/nearle/dispatch/ProfitabilitySection.js
index 7c1ac26..c1769a8 100644
--- a/src/pages/nearle/dispatch/ProfitabilitySection.js
+++ b/src/pages/nearle/dispatch/ProfitabilitySection.js
@@ -18,16 +18,20 @@ import './ProfitabilitySection.css';
// Constants
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+// Exported so reports/profitability.js can reuse the exact same profit math
+// instead of hand-duplicating it (a duplication that had already drifted
+// into a sync-risk between the two files).
+
/** Revenue rule: โน30 base for โค8 km, โน6/km beyond. */
-const BASE_REVENUE = 30;
-const BASE_KM_LIMIT = 8;
-const EXTRA_RATE_KM = 6;
+export const BASE_REVENUE = 30;
+export const BASE_KM_LIMIT = 8;
+export const EXTRA_RATE_KM = 6;
/** Fixed salary cost sliced per slot (โน5000 / 30 days / 1 slot). */
-const FIXED_COST_PER_SLOT = 500 / 3;
+export const FIXED_COST_PER_SLOT = 500 / 3;
/** Variable fuel / wear cost per km. */
-const VARIABLE_RATE_KM = 2.5;
+export const VARIABLE_RATE_KM = 2.5;
/** Status display config keyed by normalised status string. */
const STATUS_MAP = {
@@ -54,7 +58,7 @@ function orderRevenue(order) {
return km <= BASE_KM_LIMIT ? BASE_REVENUE : BASE_REVENUE + (km - BASE_KM_LIMIT) * EXTRA_RATE_KM;
}
-const BATCHES_DEFAULT = [
+export const BATCHES_DEFAULT = [
{ id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 },
{ id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12.5 },
{ id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 }
@@ -77,7 +81,16 @@ const getRowBatch = (r, batches = BATCHES_DEFAULT) => {
return getBatchForHour(d.hour() + d.minute() / 60, batches);
};
+// Orders in these statuses never delivered, so they earn no revenue and
+// shouldn't be charged fuel/slot cost either โ including them here made
+// every rider's profit/margin KPI count cancelled and skipped stops as if
+// they'd been completed.
+const EXCLUDED_FROM_PROFIT_STATUSES = new Set(['cancelled', 'skipped']);
+
function calcRiderMetrics(rider, selectedDate, batches) {
+ // `orders` stays the full date-filtered set โ the breakdown table and
+ // order-count KPI intentionally still show cancelled/skipped stops for
+ // context. Only the money/km math below excludes them.
const orders = (rider.orders ?? []).filter((o) => {
if (!selectedDate) return true;
const dateStr = o.assigntime
@@ -87,11 +100,15 @@ function calcRiderMetrics(rider, selectedDate, batches) {
: 'unknown';
return dateStr === selectedDate;
});
+ const billableOrders = orders.filter((o) => {
+ const status = String(o.orderstatus ?? o.status ?? '').toLowerCase();
+ return !EXCLUDED_FROM_PROFIT_STATUSES.has(status);
+ });
let revenue = 0;
let kms = 0;
const slotsByDate = {};
- for (const o of orders) {
+ for (const o of billableOrders) {
revenue += orderRevenue(o);
kms += parseFloat(o.riderkms || 0);
diff --git a/src/pages/nearle/hubs/hubs.js b/src/pages/nearle/hubs/hubs.js
index e68194d..fb41168 100644
--- a/src/pages/nearle/hubs/hubs.js
+++ b/src/pages/nearle/hubs/hubs.js
@@ -65,7 +65,10 @@ const emptyForm = {
latitude: '',
longitude: '',
pincode: '',
- status: 'active'
+ // Confirmed live: POST /admin/hubs rejects lowercase status ("active"
+ // fails, "Active" works) โ Inactive was never confirmed to work even on
+ // create, so this only offers the confirmed-working value here.
+ status: 'Active'
};
const Hubs = () => {
@@ -138,7 +141,7 @@ const Hubs = () => {
latitude: row.latitude ?? '',
longitude: row.longitude ?? '',
pincode: row.pincode || '',
- status: row.status || 'active'
+ status: row.status || 'Active'
});
setDialogOpen(true);
};
@@ -353,8 +356,8 @@ const Hubs = () => {
Status
diff --git a/src/pages/nearle/login.js b/src/pages/nearle/login.js
index 080687b..754baf2 100644
--- a/src/pages/nearle/login.js
+++ b/src/pages/nearle/login.js
@@ -169,7 +169,6 @@ const Login = () => {
label="E-mail Address"
type="email"
size="lg"
- isRequired
value={email}
onChange={(value) => setEmail(value.toLocaleLowerCase())}
/>
@@ -177,7 +176,6 @@ const Login = () => {
label="Password"
type="password"
size="lg"
- isRequired
value={password}
onChange={(value) => setPassword(value)}
/>
@@ -189,13 +187,13 @@ const Login = () => {
{/* footer */}
-
+
© All rights reserved
-
+
Terms and Conditions
-
+
Privacy Policy
diff --git a/src/pages/nearle/orders/OrdersPreview.js b/src/pages/nearle/orders/OrdersPreview.js
index eb58df9..0df13f9 100644
--- a/src/pages/nearle/orders/OrdersPreview.js
+++ b/src/pages/nearle/orders/OrdersPreview.js
@@ -187,7 +187,6 @@ const OrdersPreview = () => {
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const navigate = useNavigate();
const location = useLocation();
- console.log('location.state', location.state);
const [rider, setRider] = useState(null);
const [payment, setPayment] = useState(null);
const [finaldeliveryList, setFinalDeliveryList] = useState([]);
@@ -200,14 +199,6 @@ const OrdersPreview = () => {
const aiMode = location.state?.aiMode;
const reassignOrders = location.state?.reassignOrders;
- useEffect(() => {
- console.log('aiMode', aiMode);
- console.log('riderToken', riderToken);
- console.log('zoneData', zoneData);
- console.log('metaData', metaData);
- console.log('reassignOrders', reassignOrders);
- }, []);
-
useEffect(() => {
if (!deliverylist?.length) return;
const updateDeliveryAmtList = deliverylist.map((list) => {
@@ -228,7 +219,6 @@ const OrdersPreview = () => {
});
setFinalDeliveryList(updateDeliveryAmtList);
- console.log('finaldeliveryList', updateDeliveryAmtList);
}, [deliverylist]);
// ==============================|| fetchPaymentType ||============================== //
@@ -275,16 +265,27 @@ const OrdersPreview = () => {
const createNormalDeliveryMutation = useMutation({
mutationFn: finalCreatedeliveries, // for optimised delivery create
- onSuccess: (data, variables) => {
- console.log('data', data);
- console.log('varialbles', variables);
- notifyRiderMutation.mutate(rider?.userfcmtoken || riderToken); // Call notifyRider after success
+ onSuccess: (data) => {
+ // notifyRider expects a milerprofileid, not an FCM token โ the server
+ // looks the device up itself (see dispatch/Preview.js's identical
+ // fix). rider?.userfcmtoken/riderToken here were always the wrong
+ // type, so every notify call from this page 404'd silently. Notify
+ // every rider actually present across the committed deliveries
+ // instead, keyed by the same `userid` field this page already uses
+ // for rider identity (see the Change Rider handlers below).
+ const notifiedRiderIds = new Set();
+ (finaldeliveryList || []).forEach((order) => {
+ const riderId = order.userid;
+ if (riderId == null || notifiedRiderIds.has(String(riderId))) return;
+ notifiedRiderIds.add(String(riderId));
+ notifyRiderMutation.mutate(riderId);
+ });
if (data.status == 'accepted') {
OpenToast('Delivery Created Successfully', 'success', 2000);
}
setTimeout(() => {
setIsLoading(false);
- navigate('/nearle/orders');
+ navigate('/doormile/orders');
}, 2000);
},
onError: (error) => {
@@ -306,7 +307,7 @@ const OrdersPreview = () => {
navigate('/nearle/orders')}
+ onClick={() => navigate('/doormile/orders')}
sx={{
backgroundColor: 'action.hover',
color: 'text.primary',
@@ -696,7 +697,6 @@ const OrdersPreview = () => {
return;
}
if (newValue) {
- console.log('Selected:', newValue);
setPayment(newValue);
const newList = finaldeliveryList?.map((list) => ({
...list,
@@ -719,7 +719,6 @@ const OrdersPreview = () => {
}
if (newValue) {
setRider(newValue);
- console.log('Selected:', newValue);
const newList = finaldeliveryList?.map((list) => ({
...list,
userid: newValue.userid,
@@ -747,7 +746,7 @@ const OrdersPreview = () => {
color="secondary"
startIcon={}
onClick={() => {
- navigate('/nearle/orders');
+ navigate('/doormile/orders');
}}
>
Back
diff --git a/src/pages/nearle/orders/createorder1.js b/src/pages/nearle/orders/createorder1.js
index bf39c89..2b3ea1e 100644
--- a/src/pages/nearle/orders/createorder1.js
+++ b/src/pages/nearle/orders/createorder1.js
@@ -45,7 +45,14 @@ import { useNavigate } from 'react-router';
import { MdLocationCity } from 'react-icons/md';
import { TbMapPinCode } from 'react-icons/tb';
import { FaLocationDot } from 'react-icons/fa6';
-import { getAdminPricing, getAdminTenant, getTenantCustomers, getTenantLocations, createExpressBooking } from 'pages/api/doormileApi';
+import {
+ getAdminPricing,
+ getAdminTenant,
+ getAdminTenants,
+ getAdminCustomers,
+ getTenantLocations,
+ createExpressBooking
+} from 'pages/api/doormileApi';
import { useTheme } from '@mui/material/styles';
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
import Loader from 'components/Loader';
@@ -175,6 +182,32 @@ const SoftPaper = (props) => (
/>
);
+// Some saved locations come back from GET /admin/tenants/:id/locations with
+// a blank locationname (live-confirmed, e.g. tenant 1's only location) โ fall
+// back to the address rather than rendering a bare icon with no text.
+const locationDisplayName = (loc) => (loc?.locationname && loc.locationname.trim()) || loc?.address || 'Unnamed location';
+
+// Tenants with many locations in the same city (DailyGrubs has 23, all in
+// Coimbatore) made the old "(city)" suffix useless for telling them apart โ
+// sort primary-first then alphabetically so the picker is at least ordered
+// the same way every time; the full address is shown per-option instead
+// (see renderOption on the Business Location Autocomplete below).
+const sortLocations = (locations) =>
+ [...(locations || [])].sort((a, b) => {
+ if (!!a.isprimary !== !!b.isprimary) return a.isprimary ? -1 : 1;
+ return locationDisplayName(a).localeCompare(locationDisplayName(b));
+ });
+
+// POST /admin/expressbooking's parcels[].itemcategory is a free-text string
+// (see express-console-api.md's sample body) โ the old jupiter backend had a
+// real /utils/getsubcategories lookup table behind this dropdown, but that
+// endpoint has no equivalent on the new API (confirmed โ see
+// src/pages/api/CLAUDE.md's list of retired-with-no-replacement resources).
+// This is a fixed, curated list rather than a live lookup so the dropdown
+// actually has options; `freeSolo` lets an operator type a category that
+// isn't on the list instead of being blocked.
+const PARCEL_CATEGORIES = ['Food', 'Groceries', 'Documents', 'Electronics', 'Clothing & Apparel', 'Medicines', 'Furniture', 'Others'];
+
const Createorder1 = () => {
// ================================================= || GoogleMaps (Drawer) || =================================================
const [inputValue, setInputValue] = React.useState('');
@@ -205,6 +238,11 @@ const Createorder1 = () => {
const autocompleteService = useRef(null);
const [tenanatLocoId, setTenanatLocoId] = useState(localStorage.getItem('locationid'));
const [isLocation, setIsLocation] = useState(false);
+ // Drives what shows in place of the Business Location picker. A toast alone
+ // wasn't enough โ it's easy to miss, and an empty Autocomplete with no
+ // toast visible reads as "broken" with zero explanation. This makes the
+ // reason a persistent, visible message instead.
+ const [locationsStatus, setLocationsStatus] = useState('loading');
const textFieldRef1 = useRef(null);
const textFieldRef2 = useRef(null);
const [collectionamt, setCollectionamt] = useState(0);
@@ -269,7 +307,6 @@ const Createorder1 = () => {
const [tenantinfo, setTenantinfo] = useState({});
const [searchword, setSearchword] = useState('');
const [clientdetailarr, setClientdetailarr] = useState([]);
- const [clientdetailbusinessarr, setClientdetailbusinessarr] = useState([]);
const [admintoken, setAdmintoken] = useState();
const [tenantlocationlist, setTenantlocationlist] = useState([]);
const [tenant, setTenant] = useState({});
@@ -289,11 +326,20 @@ const Createorder1 = () => {
const [pricePerKm, setPricePerKm] = useState(0);
const [minKm, setMinKm] = useState(0);
const [totalCharge, setTotalCharge] = useState(0);
- const [subCat, setSubCat] = useState([]);
- const [subCatName, setSubCatName] = useState('Select ');
- const [subCatId, setSubCatId] = useState();
+ const [category, setCategory] = useState(null);
const [weight, setWeight] = useState('');
- const tid = localStorage.getItem('tenantid');
+ // Per express-console-api.md's "Tenant scoping" section: token tenantid
+ // 0/null means Doormile's own staff, who can see/act on every tenant โ
+ // NOT an error state. A client (tenant) login carries a real tenantid and
+ // never needs this picker at all. loggedInTenantId is what's actually on
+ // the account; selectedTenantId is what the page operates as right now
+ // (auto-filled for a client login, chosen from the picker for staff).
+ const loggedInTenantId = localStorage.getItem('tenantid');
+ const isStaffLogin = !loggedInTenantId || loggedInTenantId === '0';
+ const [selectedTenantId, setSelectedTenantId] = useState(isStaffLogin ? '' : loggedInTenantId);
+ const [staffTenantList, setStaffTenantList] = useState([]);
+ const [staffTenantListLoading, setStaffTenantListLoading] = useState(false);
+ const tid = selectedTenantId;
const [selectedCatChip, setSelectedCatChip] = useState(null);
const [isCustomerOpen, setIsCustomerOpen] = useState(false);
const [searchCustList, setSearchCustList] = useState('');
@@ -317,10 +363,6 @@ const Createorder1 = () => {
const [numErr2, setNumErr2] = useState(false);
const [isSms, setIsSms] = useState(0);
- useEffect(() => {
- console.log(isSms);
- }, [isSms]);
-
const handleChipClick = (chipLabel) => {
setSelectedCatChip(chipLabel);
};
@@ -338,23 +380,24 @@ const Createorder1 = () => {
const fetchTenantPricing = async () => {
try {
const pricing = (await getAdminPricing()) || [];
- const match = pricing.find((p) => p.tenantid === tid);
+ // tid comes from localStorage (always a string) but pricing.tenantid is
+ // numeric โ a strict === here never matched, so basePrice/pricePerKm/
+ // minKm stayed at their 0 defaults and every order priced at โน0.
+ const match = pricing.find((p) => String(p.tenantid) === String(tid));
if (match) {
setBasePrice(match.baseprice);
setPricePerKm(match.priceperkm);
setMinKm(match.basedistance);
}
} catch (error) {
- console.log('fetchTenantPricing error', error);
+ // swallow โ pricing defaults to 0 and stays there, handled upstream
}
};
useEffect(() => {
- fetchTenantPricing();
- }, []);
+ if (tid) fetchTenantPricing();
+ }, [tid]);
useEffect(() => {
- console.log('startPoint', startPoint);
- console.log('endPoint', endPoint);
if (startPoint.latitude != 0 && startPoint.longitude != 0 && endPoint.latitude != 0 && endPoint.longitude != 0) {
// getDistance();
calculateDistance(startPoint, endPoint);
@@ -363,10 +406,8 @@ const Createorder1 = () => {
const getDistance = () => {
const dist = geolib.getPreciseDistance(startPoint, endPoint, 1);
- console.log(`Distance: ${dist} meters`);
const distanceInKm = (dist / 1000).toFixed(2);
const roundedDistance = Math.round(distanceInKm);
- console.log('roundedDistance', roundedDistance);
setDistance(roundedDistance.toFixed(2));
if (roundedDistance < minKm) {
setTotalCharge(basePrice);
@@ -384,11 +425,9 @@ const Createorder1 = () => {
const calculateDistance = async (pickup, drop) => {
try {
const roundedDistance = await calculateDrivingDistance(pickup, drop);
- console.log('calculated distance in km:', roundedDistance);
setDistance(roundedDistance);
const total = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm);
- console.log('total charge:', total);
setTotalCharge(total);
setShowDistance(true);
@@ -396,11 +435,6 @@ const Createorder1 = () => {
setShowDistance(true);
setOpen4(true);
}
-
- // โฑ๏ธ Approximate duration
- const avgSpeed = 40; // km/h (adjust if needed)
- const duration = Math.round((roundedDistance / avgSpeed) * 60); // minutes
- console.log(`Distance: ${roundedDistance}, Duration: ${duration}`);
} catch (error) {
console.error('Error calculating distance:', error);
}
@@ -442,7 +476,6 @@ const Createorder1 = () => {
val.contactno.toLowerCase().includes(searchword.toLowerCase())
);
});
- console.log(arr);
setClientdetailarr([...arr]);
} else {
setClientdetailarr([...clientdetail]);
@@ -458,22 +491,18 @@ const Createorder1 = () => {
setTenant(info);
setTenantid(info.tenantid);
fetchAppAdminTokens();
- setSubCatId(info.subcategoryid);
}
} catch (err) {
- console.log(err);
+ // doormileAxios's response interceptor rejects with error.response.data
+ // (not the raw axios error), so the message lives at err.message here.
+ opentoast(err?.message || 'Could not load tenant info', 'error', 3000);
} finally {
setLoading(false);
}
};
useEffect(() => {
- fetchtenantinfo();
- }, []);
- // No item-subcategory lookup endpoint in the new API.
- const getsubcategories = async () => setSubCat([]);
- useEffect(() => {
- getsubcategories();
- }, []);
+ if (tid) fetchtenantinfo();
+ }, [tid]);
// ==================================================== || fetchTiming || ====================================================
// No per-zone open/close-hours or geofence-radius endpoint in the new API
@@ -523,7 +552,6 @@ const Createorder1 = () => {
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: time ? time : 2000
});
- console.log(alertmessage);
};
function closeAddressModal() {
@@ -538,29 +566,25 @@ const Createorder1 = () => {
const createsubmitobj2 = async () => {
// POST /admin/expressbooking's schema is a single flat booking object
- // (tenantid, pickuplocationid, customer_phone/name, delivery* fields,
+ // (tenantid, pickup* fields, customer_phone/name, delivery* fields,
// service_option, finalprice, notes, parcels[]) โ nothing like the old
// pickup/drop/orders triple. The drop contact becomes the booking's
- // customer (new API models the delivery recipient as "the customer");
- // the pickup side maps to pickuplocationid when a saved location was
- // picked (tenanatLocoId/isLocation, set by the "Business Location"
- // Autocomplete below โ pickCust.deliverylocationid was always '' and
- // never actually got wired to the picker, so this shortcut never fired),
- // otherwise raw pickup* fields following the doc's own delivery*/pickup*
- // naming symmetry (pickupaddress/pickuppincode/pickupcity/pickuplatitude/
- // pickuplongitude โ NOT shown in the doc's example, so unverified against
- // the live API).
+ // customer (new API models the delivery recipient as "the customer").
+ //
+ // Always send raw pickupaddress/pickuppincode/pickupcity/pickuplatitude/
+ // pickuplongitude, even when a saved location was picked (the Business
+ // Location Autocomplete + gettenantlocations()'s single-location
+ // auto-select both copy the chosen location's fields into pickCust) โ
+ // live-confirmed the pickuplocationid shortcut currently 500s on
+ // POST /admin/expressbooking (backend bug, reported; not something a
+ // frontend fix can work around other than avoiding the field).
const arr = {
- tenantid: tenant.tenantid,
- ...(isLocation && tenanatLocoId
- ? { pickuplocationid: tenanatLocoId }
- : {
- pickupaddress: pickCust.address || '',
- pickuppincode: pickCust.postcode || '',
- pickupcity: pickCust.city || '',
- pickuplatitude: Number(pickCust.latitude) || 0,
- pickuplongitude: Number(pickCust.longitude) || 0
- }),
+ tenantid: Number(tid) || tenant.tenantid,
+ pickupaddress: pickCust.address || '',
+ pickuppincode: pickCust.postcode || '',
+ pickupcity: pickCust.city || '',
+ pickuplatitude: Number(pickCust.latitude) || 0,
+ pickuplongitude: Number(pickCust.longitude) || 0,
customer_phone: dropCust.contactno || '',
customer_name: dropCust.firstname || '',
deliveryaddress: dropCust.address || '',
@@ -573,14 +597,15 @@ const Createorder1 = () => {
notes: otherinstructions || '',
parcels: [
{
- itemcategory: subCat.find((c) => c.subcategoryid === subCatId)?.categoryname || 'General',
+ itemcategory: category || 'General',
itemdescription: 'Order',
declaredvalue: +totalCharge.toFixed(2) || 0
}
]
};
- console.log('createsubmitobj2', arr);
- if (!pickCust.firstname) {
+ if (!arr.tenantid) {
+ opentoast('Pick a tenant before creating an order', 'warning', 2000);
+ } else if (!pickCust.firstname) {
opentoast('Enter Pickup Contact Name ', 'warning', 2000);
} else if (!pickCust.contactno) {
opentoast('Enter Pickup Contact Number ', 'warning', 2000);
@@ -610,8 +635,6 @@ const Createorder1 = () => {
opentoast('Enter Drop Landmark ', 'warning', 2000);
} else if (!selectedtime) {
opentoast('Choose deliverytime ', 'warning', 2000);
- } else if (!setSubCatId) {
- opentoast('Choose SubCategory ', 'warning', 2000);
} else {
setLoading(true);
try {
@@ -622,82 +645,65 @@ const Createorder1 = () => {
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1000
});
- navigate('/nearle/orders');
+ navigate('/doormile/orders');
} else {
opentoast(createRes.message || 'Something went wrong, Cannot create order', 'warning');
}
} catch (error) {
- console.log('createResErr', error);
// Previously silent โ a thrown error here (network failure, CityGate
// rejection, validation error) left the operator with no feedback at
// all and a permanently stuck loading spinner (setLoading(false) was
- // only reachable on the success path).
- opentoast(error.response?.data?.message || error.message || 'Cannot create order', 'error', 3000);
+ // only reachable on the success path). doormileAxios's response
+ // interceptor already unwraps to error.response.data before
+ // rejecting, so the message lives at error.message, not
+ // error.response.data.message.
+ opentoast(error?.message || 'Cannot create order', 'error', 3000);
} finally {
setLoading(false);
}
}
};
- useEffect(() => {
- console.log('shiftarr');
- console.log(shiftarr);
- }, [shiftarr]);
-
const clicked1 = (e) => {
setShift(e);
};
const dialogopen = (i, result) => {
- console.log(i, result);
setOpen({ shiftsno: result.sno, sno: i.sno });
};
const dialogclose = () => {
setOpen('');
};
// ========================================================= || clientdetails || =========================================================
+ // GET /admin/customers (B2C directory), not /admin/tenantcustomers โ that
+ // resource is a manually-added contact book with nothing in it unless
+ // someone explicitly creates an entry. This one is confirmed to auto-grow:
+ // POST /admin/expressbooking's customer_phone/customer_name "creates a
+ // Guest customer if unknown" (see express-console-api.md), so every order
+ // placed lands a reusable contact here โ matching the old jupiter flow
+ // where placing an order (customerid: 0) auto-saved the customer too.
+ // Field shape is { appcustomerid, createdat, email, name, phone,
+ // totalbookings } โ a single `name`, not firstname/lastname, and still no
+ // address at all (see the Saved Contacts dialog below).
const clientdetails = async () => {
setLoading2(true);
try {
- const customers = (await getTenantCustomers()) || [];
+ const customers = (await getAdminCustomers()) || [];
const filtered = searchCustList
- ? customers.filter(
- (val) =>
- val.firstname?.toLowerCase().includes(searchCustList.toLowerCase()) ||
- val.phone?.includes(searchCustList) ||
- val.contactno?.includes(searchCustList)
- )
+ ? customers.filter((val) => val.name?.toLowerCase().includes(searchCustList.toLowerCase()) || val.phone?.includes(searchCustList))
: customers;
setClientdetail(filtered);
setCustomerlist(filtered);
- setClientdetailarr(filtered.map((val) => ({ label: `${val.firstname} | ${val.phone || val.contactno}`, ...val })));
+ setClientdetailarr(filtered.map((val) => ({ label: `${val.name} | ${val.phone}`, ...val })));
} catch (err) {
- console.log(err);
opentoast('server error', 'warning');
} finally {
setLoading2(false);
}
};
useEffect(() => {
- console.log('clientdetails, by search');
clientdetails();
- // clientdetailsbusiness();
}, [searchCustList.length > 3, searchCustList == '']);
- // ========================================================= || clientdetailsbusiness || =========================================================
- const clientdetailsbusiness = async () => {
- setLoading2(true);
- try {
- const customers = (await getTenantCustomers()) || [];
- setClientdetail(customers);
- setClientdetailbusinessarr(customers.map((val) => ({ label: `${val.firstname} | ${val.phone || val.contactno}`, ...val })));
- } catch (err) {
- console.log(err);
- opentoast('server error', 'warning');
- } finally {
- setLoading2(false);
- }
- };
-
// ================================================== || sendnotifications || ==================================================
// No bulk-notify endpoint in the new API โ never actually called (dead in
// the original code too, only referenced from a commented-out call site).
@@ -827,20 +833,69 @@ const Createorder1 = () => {
// ============================================= || gettenantlocations (branches) || =============================================
const gettenantlocations = async () => {
+ if (!tid) {
+ // Staff login (express-console-api.md: token tenantid 0/null = Doormile
+ // staff, who can act on any tenant) with no tenant picked yet โ the
+ // picker UI covers this, not an error. A genuinely tenant-less,
+ // non-staff account can't get here at all (isStaffLogin would be true).
+ setLocationsStatus('no-tenant');
+ return;
+ }
+ setLocationsStatus('loading');
try {
const locations = (await getTenantLocations(tid)) || [];
setTenantlocations(locations);
- if (locations.length == 1) {
+ if (locations.length === 1) {
+ setLocationsStatus('ready');
setIsLocation(true);
- setTenanatLocoId(locations[0].locationid);
+ setTenanatLocoId(locations[0].tenantlocationid);
+ const loc = locations[0];
+ setPickCust((prev) => ({
+ ...prev,
+ address: loc.address || '',
+ suburb: loc.locationname || loc.city || '',
+ city: loc.city || '',
+ postcode: loc.pincode || '',
+ landmark: loc.address || '',
+ latitude: loc.latitude,
+ longitude: loc.longitude
+ }));
+ } else if (locations.length === 0) {
+ // No saved locations for this tenant โ every pickup field below is a
+ // plain manual TextField anyway, so let the operator fill it in by
+ // hand instead of leaving the form permanently disabled.
+ setLocationsStatus('empty');
+ setIsLocation(true);
+ opentoast('No saved business locations โ enter pickup details manually below', 'info', 3000);
+ } else {
+ setLocationsStatus('ready');
}
} catch (err) {
- console.log('gettenantlocations', err);
+ // doormileAxios's response interceptor rejects with error.response.data
+ // (not the raw axios error) โ so the message is err.message here, not
+ // err.response.data.message (that was reading through a layer that no
+ // longer exists at this point and always fell back to the generic text).
setTenantlocations([]);
+ setLocationsStatus('error');
+ opentoast(err?.message || 'Could not load business locations', 'error', 4000);
}
};
useEffect(() => {
gettenantlocations();
+ }, [tid]);
+
+ // ============================================= || fetchStaffTenantList (Doormile staff picker) || =============================================
+ // Only staff logins (loggedInTenantId blank/0) need this โ a client login's
+ // tenant is already fixed by their token and this never fetches for them.
+ useEffect(() => {
+ if (!isStaffLogin) return;
+ setStaffTenantListLoading(true);
+ getAdminTenants()
+ .then((tenants) => setStaffTenantList((tenants || []).filter((t) => t.status === 'Active')))
+ .catch((err) => {
+ opentoast(err?.message || 'Could not load tenant list', 'error', 4000);
+ })
+ .finally(() => setStaffTenantListLoading(false));
}, []);
return (
@@ -853,7 +908,7 @@ const Createorder1 = () => {
elevation={0}
sx={{
position: 'sticky',
- top: '60px',
+ top: 'var(--appshell-header-height, 64px)',
zIndex: 1000,
mb: 1.5,
p: { xs: 1.5, sm: 2, md: 2.5 },
@@ -911,7 +966,124 @@ const Createorder1 = () => {
- {tenantLocations.length === 1 ? (
+ {locationsStatus === 'loading' ? (
+
+ Loading business locationsโฆ
+
+ ) : locationsStatus === 'no-tenant' && isStaffLogin ? (
+ // Staff login (express-console-api.md: token tenantid 0/null
+ // = "Doormile's own staff... sees everything, all tenants")
+ // โ pick which tenant to create this order for. Everything
+ // downstream (pricing, saved locations, the booking's own
+ // tenantid) is keyed off `tid`, which equals selectedTenantId.
+ option?.tenantname || ''}
+ isOptionEqualToValue={(option, value) => option?.tenantid === value?.tenantid}
+ PaperComponent={SoftPaper}
+ sx={{ width: { xs: '100%', sm: 300 } }}
+ size="small"
+ onChange={(event, value) => setSelectedTenantId(value ? String(value.tenantid) : '')}
+ renderInput={(params) => (
+
+
+
+
+
+ )
+ }}
+ sx={{
+ '& .MuiOutlinedInput-root': {
+ borderRadius: DT.radiusPill + 'px',
+ bgcolor: '#fff',
+ fontWeight: 600,
+ '& fieldset': { borderColor: edge(BRAND), borderWidth: 1.5 },
+ '&:hover fieldset': { borderColor: BRAND },
+ '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` },
+ '&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 2 }
+ }
+ }}
+ />
+ )}
+ />
+ ) : locationsStatus === 'no-tenant' ? (
+
+ This login has no tenant assigned โ sign in as a tenant user
+
+ ) : locationsStatus === 'error' ? (
+
+ Could not load business locations โ tap to retry
+
+ ) : locationsStatus === 'empty' ? (
+
+ No saved locations โ enter pickup details manually below
+
+ ) : tenantLocations.length === 1 ? (
{
fontSize: 13
}}
>
- {tenantLocations[0].locationname}
+ {locationDisplayName(tenantLocations[0])}
) : (
(option && option.locationname ? `${option.locationname} (${option.suburb || ''})` : '')}
- isOptionEqualToValue={(option, value) => option?.locationid === value?.locationid}
+ options={sortLocations(tenantLocations)}
+ getOptionLabel={(option) => locationDisplayName(option)}
+ isOptionEqualToValue={(option, value) => option?.tenantlocationid === value?.tenantlocationid}
+ renderOption={(props, option) => (
+
+
+
+
+ {locationDisplayName(option)}
+
+ {option.isprimary && (
+
+ PRIMARY
+
+ )}
+
+
+ {option.address || option.city || ''}
+
+
+
+ )}
PaperComponent={SoftPaper}
- sx={{ width: { xs: '100%', sm: 300 } }}
+ sx={{ width: { xs: '100%', sm: 320 } }}
size="small"
renderInput={(params) => (
{
)}
onChange={(event, value, reason) => {
if (value) {
- setTenanatLocoId(value.locationid);
+ setTenanatLocoId(value.tenantlocationid);
setIsLocation(true);
+ // Also fill pickCust's raw address fields from the
+ // picked location โ POST /admin/expressbooking's
+ // pickuplocationid currently 500s server-side (live-
+ // confirmed), so createsubmitobj2() always sends raw
+ // pickup* fields instead; this keeps that path working
+ // when a saved location is chosen here.
+ setPickCust((prev) => ({
+ ...prev,
+ address: value.address || '',
+ suburb: value.locationname || value.city || '',
+ city: value.city || '',
+ postcode: value.pincode || '',
+ landmark: value.address || '',
+ latitude: value.latitude,
+ longitude: value.longitude
+ }));
}
if (reason === 'clear') {
setIsLocation(false);
@@ -1024,7 +1242,7 @@ const Createorder1 = () => {
}
}}
>
- Saved Locations
+ Saved Contacts
}
/>
@@ -1097,7 +1315,6 @@ const Createorder1 = () => {
{
}
}}
>
- Saved Locations
+ Saved Contacts
}
/>
@@ -1418,7 +1635,6 @@ const Createorder1 = () => {
opentoast('Select Another Customer', 'error');
setSearchCustList('');
} else {
- console.log('DropClient', val);
setDropCust(val);
setEndPoint({ latitude: val.latitude, longitude: val.longitude });
setAddId2(1);
@@ -1513,7 +1729,6 @@ const Createorder1 = () => {
{
format="DD-MM-YYYY"
onChange={(e) => {
let dateres11 = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd');
- console.log('dateres11');
- console.log(dateres11);
setSelectedtime('');
if (dateres11 <= 0) {
- console.log('startdate', e);
setStartdate(e);
setEnddate(e);
@@ -1881,20 +2093,16 @@ const Createorder1 = () => {
Category
`${option.subcategoryname}` || ''}
+ options={PARCEL_CATEGORIES}
+ value={category}
sx={{ my: 2, zIndex: '100' }}
fullWidth
- renderInput={(params) => (
-
- )}
- onChange={(event, value, reason) => {
- if (value) {
- console.log(value);
- setSubCatName(value.subcategoryname);
- setSubCatId(value.subcategoryid);
- }
+ renderInput={(params) => }
+ onChange={(event, value) => setCategory(value || null)}
+ onInputChange={(event, value, reason) => {
+ if (reason === 'input') setCategory(value || null);
}}
/>
@@ -2126,10 +2334,10 @@ const Createorder1 = () => {
- Saved Locations
+ Saved Contacts
- Pick a saved customer for {pickordrop === 1 ? 'Pickup' : 'Drop'}
+ Pick a saved contact's name & phone for {pickordrop === 1 ? 'Pickup' : 'Drop'} โ address isn't stored, search it below
@@ -2179,32 +2387,38 @@ const Createorder1 = () => {
- No saved locations
+ No saved contacts
-
- {searchCustList ? 'Try a different keyword.' : 'Save a customer from the order form to reuse it later.'}
+
+ {searchCustList ? 'Try a different keyword.' : 'Place an order with a new contact โ it will show up here next time.'}
) : (
{customerlist.map((address, index) => {
- const isUsed = pickCust.customerid === address.customerid;
+ // GET /admin/customers (B2C directory) โ { appcustomerid,
+ // createdat, email, name, phone, totalbookings }. No
+ // address/coordinates at all (that was a jupiter-only field
+ // on the old customer record). Picking a saved contact here
+ // only pre-fills the name & phone; the operator still has
+ // to search the address below like a brand-new contact.
+ const contactPhone = address.phone;
+ const isUsed = !!contactPhone && pickCust.phone === contactPhone;
return (
{
if (isUsed) return;
setIsCustomerOpen(false);
+ const contact = { firstname: address.name, contactno: contactPhone, phone: contactPhone };
if (pickordrop === 1) {
setAddId1(1);
- setStartPoint({ latitude: address.latitude, longitude: address.longitude });
- setPickCust(address);
- setPickNum(address.contactno);
+ setPickCust((prev) => ({ ...prev, ...contact }));
+ setPickNum(contactPhone);
} else {
setAddId2(1);
- setEndPoint({ latitude: address.latitude, longitude: address.longitude });
- setDropCust(address);
- setdropNum(address.contactno);
+ setDropCust((prev) => ({ ...prev, ...contact }));
+ setdropNum(contactPhone);
}
}}
sx={{
@@ -2229,14 +2443,14 @@ const Createorder1 = () => {
}}
>
- {String(address.firstname || '?')
+ {String(address.name || '?')
.charAt(0)
.toUpperCase()}
- {address.firstname}
+ {address.name || 'Unnamed'}
{
fontWeight: 800
}}
>
- {address.contactno}
+ {contactPhone || 'โ'}
{isUsed && (
{
)}
-
- {address.address}
+
+ {address.email || 'No address on file โ search it below'}
diff --git a/src/pages/nearle/orders/details.js b/src/pages/nearle/orders/details.js
deleted file mode 100644
index 97a4732..0000000
--- a/src/pages/nearle/orders/details.js
+++ /dev/null
@@ -1,2344 +0,0 @@
-/* eslint-disable no-unused-vars */
-import {
- useEffect, useState, Fragment
- // useReducer
-} from 'react';
-import BorderColorIcon from '@mui/icons-material/BorderColor';
-import {
- // Navigate,
- // useSearchParams,
- useLocation,
- useNavigate
-} from 'react-router-dom';
-import axios from 'axios';
-import ArrowBackIcon from '@mui/icons-material/ArrowBack';
-import {
- // UserOutlined,
- EnvironmentOutlined,
- EditTwoTone
- // DeleteTwoTone
-} from '@ant-design/icons';
-// import WomanIcon from '@mui/icons-material/Woman';
-// import { Link } from 'react-router-dom';
-// import SoupKitchenIcon from '@mui/icons-material/SoupKitchen';
-import DirectionsCarIcon from '@mui/icons-material/DirectionsCar';
-import SendIcon from '@mui/icons-material/Send';
-import { KeyboardArrowUp, KeyboardArrowDown } from '@mui/icons-material'
-
-// import { PopupTransition } from 'components/@extended/Transitions';
-// import { useDispatch } from 'react-redux';
-// import { openSnackbar } from 'store/reducers/snackbar';
-// assets
-import { DeleteFilled, NotificationOutlined } from '@ant-design/icons';
-var utc = require('dayjs/plugin/utc')
-// import { groupBy } from "core-js/actual/array/group-by";
-// import "lodash.chunk";
-// var chunk = require('lodash.chunk');
-import {
- Grid,
- Typography,
- Stack,
- Button as Button1,
- // Box,
- FormControl,
- Table,
- TableHead,
- TableRow,
- TableCell,
- TableContainer,
- TableBody,
- Chip,
- Divider,
- CardActions,
- IconButton,
- Avatar,
- Dialog,
- DialogTitle,
- DialogContent,
- Checkbox,
- Tooltip,
- Button,
- Tab,
- Tabs,
- Skeleton,
- // DialogContent,
- DialogActions,
- TextField,
- CircularProgress,
- Collapse,
- Alert,
- AlertTitle,
- Link
- // Card,
- // CardHeader
-} from '@mui/material';
-
-import { PopupTransition } from 'components/@extended/Transitions';
-
-// import ControlPointIcon from '@mui/icons-material/ControlPoint';
-import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined';
-import MainCard from 'components/MainCard';
-import Loader from 'components/Loader';
-// import AlertCustomerDelete from 'sections/apps/customer/AlertCustomerDelete';
-import dayjs from 'dayjs';
-dayjs.extend(utc)
-// import { Link as RouterLink } from 'react-router-dom';
-// import PlayCircleFilled from '@mui/icons-material/PlayCircleFilled';
-// import SmileFilled from '@mui/icons-material/Mood';
-// import HeartFilled from '@mui/icons-material/Favorite';
-
-import { useTheme } from '@mui/material/styles';
-import {
- CloseOutlined,
- WarningOutlined,
- WarningFilled
- // PlusOutlined,
- // EyeTwoTone,
- // EditTwoTone,
- // DeleteTwoTone
-} from '@ant-design/icons';
-import { enqueueSnackbar } from 'notistack';
-import {
- MdLocalShipping,
- MdHourglassEmpty,
- MdCheckCircle,
- MdCancel,
- MdAccessTime,
- MdHistoryToggleOff,
- MdAssignmentTurnedIn,
- MdEdit,
- MdArrowBack,
- MdReceiptLong
-} from 'react-icons/md';
-import { Paper, Box } from '@mui/material';
-
-// ============================================================================
-// Design tokens โ shared with the rest of the redesigned operator pages.
-// ============================================================================
-const DT = {
- radiusPill: 999,
- radiusCard: 16,
- shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
- shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
- shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
- textPrimary: '#0f172a',
- textSecondary: '#64748b',
- textMuted: '#94a3b8',
- borderSubtle: '#e2e8f0',
- divider: '#f1f5f9',
- surface: '#ffffff',
- surfaceAlt: '#f8fafc'
-};
-const dtA = (c, suffix) => `${c}${suffix}`;
-const tint = (c) => dtA(c, '08');
-const soft = (c) => dtA(c, '18');
-const ring = (c) => dtA(c, '26');
-const edge = (c) => dtA(c, '55');
-
-const BRAND = '#C01227';
-const BRAND_LIGHT = '#D25463';
-
-// Semantic per-status palette โ also drives StatusBadge.
-const STATUS_META = {
- pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
- assigned: { label: 'Assigned', color: '#0ea5e9', icon: MdAssignmentTurnedIn },
- confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
- modified: { label: 'Modified', color: '#06b6d4', icon: MdHistoryToggleOff },
- processing: { label: 'Processing', color: BRAND, icon: MdAccessTime },
- active: { label: 'Active', color: '#8b5cf6', icon: MdLocalShipping },
- closed: { label: 'Closed', color: '#06b6d4', icon: MdCheckCircle },
- completed: { label: 'Completed', color: '#10b981', icon: MdCheckCircle },
- cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
-};
-
-const StatusBadge = ({ status, size = 'md' }) => {
- if (!status) return null;
- const meta = STATUS_META[String(status).toLowerCase()] || {
- label: status,
- color: DT.textMuted,
- icon: MdHistoryToggleOff
- };
- const Icon = meta.icon;
- const px = size === 'lg' ? 1.25 : 1;
- const py = size === 'lg' ? 0.5 : 0.375;
- const fs = size === 'lg' ? 12 : 11;
- return (
-
- {meta.label}
-
- );
-};
-
-const Details = () => {
- // const [searchParams] = useSearchParams();
- const [orderarr, setOrderarr] = useState([]);
- const [loading, setLoading] = useState(true);
- const location = useLocation();
- const [orderid, setOrderid] = useState('');
- const [eventlocation, setEventlocation] = useState('');
- const [orderdate, setOrderdate] = useState('');
- const [eventname, setEventname] = useState('');
- const [open, setOpen] = useState(false);
- const [clientname, setClientname] = useState('');
- // const [duedate, setDuedate] = useState('');
- const [tenantaddress, setTenantaddress] = useState('');
- const [dialogopen, setDialogopen] = useState(false);
- const [orderstatus, setOrderstatus] = useState('');
- const [currentrole, setCurrentrole] = useState('');
- const [taxamount, setTaxamount] = useState('');
- const [subtotal, setSubtotal] = useState('');
- const [grandtotal, setGrandtotal] = useState('');
- const [venuetype, setVenuetype] = useState('');
- const [orderaddons, setOrderaddons] = useState([]);
- const [otherinstructions, setOtherinstructions] = useState('');
- const [cancelleddate, setCancelleddate] = useState('');
- const [stafflist, setStafflist] = useState([]);
- const [staffarr, setStaffarr] = useState([]);
- const [orderheaderid, setOrderheaderid] = useState('');
- const [tenantid, setTenantid] = useState('');
- const [starttime, setStarttime] = useState('');
- const [endtime, setEndtime] = useState('');
- // const [orderstatus,setOrderstatus]=useStatus('');
- const [pendingtime, setPendingtime] = useState('');
- // const [processdate,setProcessdate]=useState('');
- const [orderdetailid, setOrderdetailid] = useState('');
- const [productid, setProductid] = useState('');
- const [categoryarr, setcategoryarr] = useState([])
- const [currentshiftobj, setCurrentshiftobj] = useState({
- shifts: 0,
- assigned: 0,
- remaining: 0,
- shiftid: 0,
- price: 0
- });
- const [tabstatus, setTabstatus] = useState(0)
- const { state } = useLocation();
- const [assignedpendingcount, setAssignedpendingcount] = useState('');
- // const [, forceUpdate] = useReducer(x => x + 1, 0);
- // const dispatch = useDispatch();
- const navigate = useNavigate();
- const [expandopen, setExpandopen] = useState(['', '']);
- const [startdate, setStartdate] = useState('')
- const [invoiceeligible, setInvoiceeligible] = useState(false)
-
- useEffect(() => {
- console.log("categoryarr")
-
- console.log(orderarr, eventlocation, venuetype, starttime, endtime);
- // forceUpdate();
- }, [categoryarr])
-
- // const navigate = useNavigate();
- useEffect(() => {
- if (state) {
- setOrderheaderid(state.orderheaderid);
- setTenantid(state.tenantid);
- }
- console.log(state)
- // fetchroleslist(1)
- console.log(Date.now())
- }, [])
- useEffect(() => {
- if (state) {
- setOrderheaderid(state.orderheaderid );
- setTenantid(state.tenantid );
- }
- console.log(state)
- // fetchroleslist(1)
- console.log(Date.now())
- }, [
- state.orderheaderid, state.tenantid
- ])
-
- const theme = useTheme();
-
- // const fetchorderdetails = async () => {
- // setLoading(true);
- // await axios
- // .get(`${process.env.REACT_APP_URL}/orders/orderbyid/?orderheaderid=${orderheaderid}`)
-
- // .then((res) => {
- // console.log(res);
-
- // setLoading(false);
- // })
- // .catch((err) => {
- // console.log(err);
- // setLoading(false);
- // });
-
- // };
-
- const fetchorderaddons = async () => {
- setLoading(true);
- await axios
- .get(`${process.env.REACT_APP_URL2}/orders/getordersbystatus?tenantid=${tenantid}`)
-
- .then((res) => {
- console.log(res);
- let result = res.data.Details.find((res1) => res1.orderheaderid == orderheaderid)
- // orderheaderid
- console.log("result")
-
- console.log(result)
- setOrderaddons(result.orderaddons);
- setVenuetype(result.venuetype)
- setOtherinstructions(result.remarks)
- setStartdate(result.startdate)
-
-
-
- setClientname(result.tenantname);
- setEventlocation('');
- setTenantaddress(result.tenantaddress);
- setEventname(result.eventname);
-
-
-
- setOrderdate(dayjs(result.orderdate).format('MM/DD/YYYY') || '');
- setOrderid(result.orderid);
- setOrderstatus(result.orderstatus);
- setTaxamount(result.taxamount);
- setSubtotal(result.orderamount)
- setGrandtotal(result.ordervalue);
- setCancelleddate(dayjs(result.cancelled).format('MM/DD/YYYY') || '');
-
- // if (result.orderstatus === 'pending') {
- setPendingtime(result.pending)
- // }
-
-
- setLoading(false);
- })
- .catch((err) => {
- console.log(err);
- setLoading(false);
- });
- }
-
-
-
- const fetchorderattires = async () => {
- setLoading(true);
- await axios
- .get(`${process.env.REACT_APP_URL2}/orders/getorderdetails?orderheaderid=${orderheaderid}`)
-
- .then((res) => {
- console.log('res');
- console.log(res);
- setOrderarr(res.data.Details || []);
- // let result = res.data.Details.find((res1) => res1.orderheaderid == searchParams.get('id'))
- // orderheaderid
- // console.log(result)
- // setOrderaddons(result.orderaddons);
- // setVenuetype(result.venuetype)
- // setOtherinstructions(result.remarks)
- // console.log("res");
- // let result = _.chain(res.data.Details)
-
- // .groupBy("shiftid")
-
- // .map((value, key) => ({shiftid:key, locationaddress: value[0].locationaddress, roles: value }))
- // .value()
-
- // setcategoryarr(result);
- console.log('categoryarr');
- setcategoryarr(res.data.Details)
- console.log(res.data.Details);
-
- setLoading(false);
- })
- .catch((err) => {
- console.log(err);
- setLoading(false);
- });
- }
-
- const fetchroleslist = async (cid, starttime1, endtime1, hid, sid) => {
- let fromdate = dayjs(starttime1).subtract(59, 'minutes')
- .format('YYYY-MM-DD HH:mm:ss');
- // let todate = dayjs(endtime1).utc().format('YYYY-MM-DD HH:mm:ss');
- // let fromdate = dayjs(starttime1).utc().format('YYYY-MM-DD HH:mm:ss');
- let todate = dayjs(endtime1)
- .format('YYYY-MM-DD HH:mm:ss');
- let url1;
- if (starttime1) {
- url1 = `${process.env.REACT_APP_URL2}/staffs/pools/getstaffs/?categoryid=${cid}&headerid=${hid}&shiftid=${sid}&starttime=${fromdate}&endtime=${todate}`;
- } else {
- url1 = `${process.env.REACT_APP_URL2}/staffs/pools/getstaffs/?categoryid=${cid}&headerid=${hid}&shiftid=${sid}`
- }
- setLoading(true);
- setStafflist([])
- await axios
- // .get(`${process.env.REACT_APP_URL2}/staffs/pools/getstaffs/?categoryid=${cid}&headerid=${hid}&starttime=${fromdate}&endtime=${todate}`)
- .get(url1)
- .then((res) => {
- console.log('rolelist');
- console.log(res);
- // console.log(fromdate, todate)
- // console.log(dayjs(starttime1).format('YYYY-MM-DD HH:mm:ss'))
- // console.log(dayjs(endtime1).format('HH:mm:ss'))
-
-
- setStafflist(res.data.Details || [])
- // let result = res.data.Details.find((res1) => res1.orderheaderid == searchParams.get('id'))
- // orderheaderid
- // console.log(result)
- // setOrderaddons(result.orderaddons);
- // setVenuetype(result.venuetype)
- // setOtherinstructions(result.remarks)
- setLoading(false);
- })
- .catch((err) => {
- console.log(err);
- setLoading(false);
- });
- }
-
-
- const fetchstafflist = async (odid) => {
- setLoading(true)
- try {
- // await axios.get(`${process.env.REACT_APP_URL}/orders/orderanalytics?orderdate=${chosendate}`)
- await axios.get(`${process.env.REACT_APP_URL2}/orders/getassignedinfo?orderdetailid=${odid}`)
-
- .then((res) => {
- console.log(res)
- if (res.data.status) {
- setStafflist(res.data.Details)
- }
-
- setLoading(false)
- }).catch((err) => {
- console.log(err)
- setLoading(false)
- })
-
- } catch (err) {
- console.log(err);
- setLoading(false)
- }
-
- }
- const cancelorder = async () => {
-
- await axios.put(`${process.env.REACT_APP_URL2}/orders/cancel`, {
-
- // "Orderheaderid": parseInt(orderheaderid),
- // "Tenantid": parseInt(tenantid),
- // "Orderstatus": "cancelled",
- // "Currentdatetime": dayjs().format('YYYY-MM-DD HH:mm:ss'),
- // "Cod": false,
- // "Remarks": "",
- "orderheaderid": parseInt(orderheaderid),
- // "orderdetailid":78,
- // "shiftid":788,
- "orderstatus": "cancelled",
- "cancelled": dayjs().format('YYYY-MM-DD HH:mm:ss'),
- "unserviceable": (invoiceeligible) ? 0 : 1,
-
- })
- .then((res) => {
- console.log(res)
- if (res.data.status) {
- if (orderheaderid && tenantid) {
- // fetchorderdetails();
- fetchorderaddons();
- fetchorderattires();
- }
- }
-
- }).catch((err) => {
- console.log(err)
- })
-
- }
-
- const unassign = async (val) => {
-
- let obj = {
- orderheaderid: orderheaderid,
- orderprocessid: val.orderprocessid,
- orderdetailid: val.orderdetailid,
- orderstatus: "pending",
- pending: dayjs().format('YYYY-MM-DD HH:mm:ss'),
- // processing:0,
- // cancelled:0,
- // completed:0,
- // accepted:0,
- status: 1
- }
- console.log(obj)
-
- await axios.put(`${process.env.REACT_APP_URL2}/orders/updateprocessstatus`, obj)
- .then((res) => {
- console.log(res)
- if (res.data.message === "Successful") {
- // if (orderheaderid && tenantid) {
- enqueueSnackbar('Role unassigned successfully', {
- variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 2000
- })
- if (currentshiftobj.assigned > currentshiftobj.shifts) {
- sendunassignnotification(val)
- }
-
-
-
- fetchorderaddons();
- fetchorderattires();
- setOpen(false);
- fetchassignedcount();
- dialogclose()
- setTimeout(() => {
- fetchassignedcount();
-
- }, 2000)
- }
-
- }).catch((err) => {
- console.log(err)
- })
-
- }
-
-
- const sendunassignnotification = (val) => {
- console.log(val)
- let data2;
- let tokenarr = [val.userfcmtoken]
- let arr1 = [{
- "notificationid": 0,
- "notificationdate": dayjs().format('YYYY-MM-DD HH:mm:ss'),
- "Title": "Staff Un-Asigned",
- "message": `${val.firstname} has been Un-assigned to the order ${orderid}`,
- "configid": 2,
- "tenantid": tenantid,
- "orderheaderid": orderheaderid,
- "orderprocessid": val.orderprocessid,
- "shiftid": val.shiftid,
- "userid": val.userid,
- "orderid": orderid,
- "success": 0,
- "orderstatus": 'assigned',
- "processing": dayjs().format('YYYY-MM-DD HH:mm:ss'),
- "notifytype": 2,
- "notifyreason": 'Staff Un-Assigned'
- // "sound": "ring",
- // "click_action": "FLUTTER_NOTIFICATION_CLICK",
- // "firstname": val.firstname
-
- }];
-
-
-
-
- data2 = {
- "Title": "Staff Un-Asigned",
- "message": `A Staff has been Un-assigned to the order ${orderid}`,
- "tenantid": tenantid,
- "orderheaderid": orderheaderid,
- "orderid": orderid,
- "configid": 2,
- // "click_action": "FLUTTER_NOTIFICATION_CLICK"
- }
- let fcmmodel = {
- "priority": "high",
- "registration_ids": tokenarr,
- "data": data2,
- "notification": {
- "body": `An order has been Un-assigned ${orderid}`,
- "title": "Legendary",
- "sound": "ring",
- "content_available": true,
- "click_action": "FLUTTER_NOTIFICATION_CLICK"
- }
- }
- let grpnotifyobj = {
- "notifications": arr1,
- "fcmmodel": fcmmodel
- }
- console.log("grpnotifyobj unassign")
- console.log(grpnotifyobj)
- sendgroupnotification(grpnotifyobj)
- }
-
- useEffect(() => {
- console.log(orderheaderid);
- if (orderheaderid && tenantid) {
- // fetchorderdetails();
- fetchorderaddons();
- fetchorderattires();
- fetchassignedcount()
- // fetchuserdetails();
- console.log(location.state || '');
- // setOrderid(location.state.orderid || '');
- // setEventlocation(location.state.eventlocation || '');
- // setEventlocation(address || []);
-
- // setOrderdate(dayjs(location.state.orderdate.substring(0, 10)).format('MM/DD/YYYY') || '');
- // setDuedate(dayjs(location.state.orderdate.substring(0, 10)).format('MM/DD/YYYY') || '')
- // setEventname(location.state.eventname || '');
- // setClientname(location.state.tenantname || '')
- } else {
- setLoading(false);
- }
- // fetchorderdetails();
- console.log(orderheaderid, tenantid)
- }, [orderheaderid, tenantid, assignedpendingcount]);
-
- const handleClose = () => {
- setOpen(false);
- };
- const dialogclose = () => {
- setDialogopen(false);
- setStaffarr([]);
- setExpandopen(['', ''])
- };
- useEffect(() => {
- console.log(currentshiftobj)
- })
-
-
- const assignok = async () => {
- let arr = []
- let arr1 = [];
- staffarr.map((val) => {
- arr.push({
- "orderprocessid": 0,
- // "processdate": `${dayjs(new Date()).format('YYYY-MM-DD')} ${dayjs(new Date()).format('HH:mm:ss')}`, //current date
- "processdate": dayjs().format('YYYY-MM-DD HH:mm:ss'), //current date
- "tenantid": tenantid,
- "orderheaderid": orderheaderid,
- "orderdetailid": val.orderdetailid,////
- "productid": val.productid,/////
- "userid": val.userid,
- "orderstatus": "pending",
- "pending": `${dayjs(pendingtime).format('YYYY-MM-DD')} ${dayjs(pendingtime).format('HH:mm:ss')}`, // if pending
- "starttime": `${dayjs(starttime).format('YYYY-MM-DD')} ${dayjs(starttime).format('HH:mm:ss')}`,
- "endtime": `${dayjs(endtime).format('YYYY-MM-DD')} ${dayjs(endtime).format('HH:mm:ss')}`,
- "appuserid": parseInt(localStorage.getItem("appuserid")), //loginuserid
- "shiftid": val.shiftid,
- "userrate": val.userrate,
- "productrate": val.productrate
- })
-
- staffarr.map((val) => {
- arr1.push({
- "notificationid": 0,
- "notificationdate": dayjs().format('YYYY-MM-DD HH:mm:ss'),
- "Title": "Staff Asigned",
- "message": `A Staff has been assigned to the order ${orderid}`,
- "configid": 2,
- "tenantid": tenantid,
- "orderheaderid": orderheaderid,
- "orderprocessid": 0,
- "shiftid": val.shiftid,
- "userid": val.userid,
- "orderid": orderid,
- "sound": "ring",
- "click_action": "FLUTTER_NOTIFICATION_CLICK",
- "firstname": val.firstname
- })
-
- })
-
- })
- console.log('arr')
- console.log(arr)
-
- await axios.post(`${process.env.REACT_APP_URL2}/orders/createorderprocess`, arr)
- .then((res) => {
- console.log(res)
- if (res.data.message === "Successfully created") {
- // if (orderheaderid && tenantid) {
- // fetchorderdetails();
- // fetchorderaddons();
- // fetchorderattires();
- // }
-
-
- enqueueSnackbar('Roles assigned successfully', {
- variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 2000
- })
- // fetchroleslist(productid, '', '', orderheaderid, arr1[0].shiftid);
- console.log(productid, '', '', orderheaderid, arr1[0].shiftid)
-
-
-
-
- // arr1.map((val2) => {
- // notificationpush(val2,val2.Title);
- // })
- setDialogopen(false);
- fetchorderattires();
- fetchassignedcount();
- }
-
- }).catch((err) => {
- console.log(err)
- })
- console.log(arr)
- }
-
- const notificationpush = async (val) => {
- let fcmtoken = val.userfcmtoken
- let obj1 = {
- "notificationid": 0,
- "notificationdate": dayjs().format('YYYY-MM-DD HH:mm:ss'),
- "Title": "Staff Asigned",
- "message": `A Staff has been assigned to the order ${orderid}`,
- "configid": 2,
- "tenantid": tenantid,
- "orderheaderid": orderheaderid,
- "orderprocessid": val.orderprocessid,
- "shiftid": val.shiftid,
- "userid": val.userid,
- "orderid": orderid,
- "sound": "ring",
- "click_action": "FLUTTER_NOTIFICATION_CLICK",
- // "firstname": val.firstname
-
- };
- console.log(obj1, fcmtoken)
- await axios.post(`${process.env.REACT_APP_URL2}/utils/notification/send`,
- {
- "priority": "high",
- "registration_ids": [fcmtoken],
- "data": obj1,
- "notification": {
- "body": `A Staff has been assigned to ${orderid}`,
- "title": "Legendary",
- "sound": "ring",
- "content_available": true
- }
- }
- , {
- headers: {
- 'Authorization': `Bearer ${process.env.REACT_APP_STAFF_TOKEN}`
- }
- }
- )
- .then((res) => {
- console.log(res)
- // if(res.data.status){
- enqueueSnackbar('Notification sent successfully', {
- variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 2000
- })
- // }
- })
- .catch((err) => {
- console.log(err)
- })
-
- }
-
- const fetchassignedstaffs = async () => {
-
- // console.log(obj1)
- await axios.get(`${process.env.REACT_APP_URL2}/orders/getnotificationusers?orderheaderid=${orderheaderid}`)
- .then((res) => {
- if (res.data.status) {
- let arr1 = [];
- let data2;
- let tokenarr = []
- console.log(res)
-
- res.data.details.map((val) => {
- arr1.push({
- "notificationid": 0,
- "notificationdate": dayjs().format('YYYY-MM-DD HH:mm:ss'),
- "Title": "Staff Asigned",
- "message": `${val.staffname} has been assigned to the order ${orderid}`,
- "configid": 2,
- "tenantid": tenantid,
- "orderheaderid": orderheaderid,
- "orderprocessid": val.orderprocessid,
- "shiftid": val.shiftid,
- "userid": val.userid,
- "orderid": orderid,
- "success": 0,
- "orderstatus": 'assigned',
- "processing": dayjs().format('YYYY-MM-DD HH:mm:ss'),
- "notifytype": 1,
- "notifyreason": 'Staff Assigned'
- // "sound": "ring",
- // "click_action": "FLUTTER_NOTIFICATION_CLICK",
- // "firstname": val.firstname
-
- });
- tokenarr.push(val.userfcmtoken);
-
- })
-
-
-
- data2 = {
- "Title": "Staff Asigned",
- "message": `A Staff has been assigned to the order ${orderid}`,
- "tenantid": tenantid,
- "orderheaderid": orderheaderid,
- "orderid": orderid,
- "configid": 2,
- // "click_action": "FLUTTER_NOTIFICATION_CLICK"
- }
- let fcmmodel = {
- "priority": "high",
- "registration_ids": tokenarr,
- "data": data2,
- "notification": {
- "body": `An order has been assigned ${orderid}`,
- "title": "Legendary",
- "sound": "ring",
- "content_available": true,
- "click_action": "FLUTTER_NOTIFICATION_CLICK"
- }
- }
- let grpnotifyobj = {
- "notifications": arr1,
- "fcmmodel": fcmmodel
- }
- console.log("grpnotifyobj")
- console.log(grpnotifyobj)
- sendgroupnotification(grpnotifyobj)
- // notificationpush(obj2, val.userfcmtoken);
- // notificationpush(arr1,tokenarr);
- // enqueueSnackbar('Notifications sent successfully', {
- // variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' },
- // autoHideDuration: 2000
- // })
- }
- })
- .catch((err) => {
- console.log(err)
- })
-
- }
-
- const sendgroupnotification = async (obj1) => {
-
- console.log(obj1)
- await axios.post(`${process.env.REACT_APP_URL2}/utils/notification/sendall`, obj1, {
- headers: {
- 'Authorization': `Bearer ${process.env.REACT_APP_STAFF_TOKEN}`
- }
- }
- )
- .then((res) => {
- console.log(res)
- if (res.data.status) {
- // updateorderstatus();
- enqueueSnackbar('Notification sent successfully', {
- variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 2000
- })
- fetchorderaddons();
- }
- })
- .catch((err) => {
- console.log(err)
- })
-
- }
-
- // const updateorderstatus = async () => {
-
- // await axios.put(`${process.env.REACT_APP_URL2}/orders/updateorderstatus`,{
- // "orderheaderid":orderheaderid,
- // "tenantid":tenantid,
- // "orderstatus":"processing",
- // "pending":"",
- // "processing":dayjs().format('YYYY-MM-DD HH:mm:ss'),
- // "completed":""
- // })
- // .then((res) => {
- // console.log(res)
- // fetchorderdetails();
- // fetchorderaddons();
- // fetchorderattires();
-
- // })
- // .catch((err) => {
- // console.log(err)
- // fetchorderdetails();
- // fetchorderaddons();
- // fetchorderattires();
- // })
-
- // }
-
- const fetchassignedcount = async () => {
-
- // console.log(obj1)
- await axios.get(`${process.env.REACT_APP_URL2}/orders/getorderstatuscount?orderheaderid=${orderheaderid}`)
- .then((res) => {
- if (res.data.status) {
- // let arr1=[];
- console.log(res)
- setAssignedpendingcount(res.data.pendingcount)
- fetchorderaddons()
- // res.data.details.map((val) => {
- // let val2={
- // "notificationid": 0,
- // "notificationdate": dayjs().format('YYYY-MM-DD HH:mm:ss'),
- // "Title": "Staff Asigned",
- // "message": `A Staff has been assigned to the order ${orderid}`,
- // "configid": 2,
- // "tenantid": tenantid,
- // "orderheaderid": orderheaderid,
- // "orderprocessid": val.orderprocessid,
- // "shiftid": val.shiftid,
- // "userid": val.userid,
- // "orderid": orderid,
- // "sound": "ring",
- // "click_action": "FLUTTER_NOTIFICATION_CLICK",
- // // "firstname": val.firstname
-
- // };
-
-
- // notificationpush(val2,val.userfcmtoken);
-
-
- // })
- } else {
- setAssignedpendingcount(res.data.pendingcount)
- fetchorderaddons()
- }
- })
- .catch((err) => {
- console.log(err)
- })
-
- }
-
- function AlertCustomerDelete({
- // title,
- open, handleClose }) {
- const [deletepassword, setDeletepassword] = useState('');
-
- return (
- handleClose(false)}
- maxWidth="xs"
- PaperProps={{ sx: { borderRadius: 3 } }}
- >
-
-
-
-
-
-
-
- Cancel Order
-
-
- Confirm to permanently cancel this order
-
-
-
-
-
-
-
- {orderid.slice(4)}
-
-
- {(invoiceeligible) &&
- }>
- Order is within 24Hrs time frame. The order will be invoiced with standard pricing as agreed.
- Terms & Condition link
-
- }
-
-
- Please type in the order number to confirm.
-
- setDeletepassword(e.target.value)}
- error={deletepassword !== orderid.slice(4)}
- value={deletepassword}
- placeholder={orderid.slice(4)}
- />
-
-
- handleClose(false)}
- variant="outlined"
- sx={{
- borderRadius: 999,
- py: 1,
- borderColor: DT.borderSubtle,
- color: DT.textSecondary,
- fontWeight: 700,
- '&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
- }}
- >
- No
-
- {
- if (deletepassword === orderid.slice(4)) {
- cancelorder();
- handleClose(true);
- }
- }}
- sx={{
- borderRadius: 999,
- py: 1,
- bgcolor: '#ef4444',
- fontWeight: 700,
- boxShadow: `0 6px 18px ${ring('#ef4444')}`,
- '&:hover': { bgcolor: '#dc2626' }
- }}
- >
- Yes, Cancel
-
-
-
-
-
- );
- }
-
- return (
- <>
- {loading && }
-
-
-
-
-
-
-
-
-
-
-
-
-
- Assign Roles
-
-
- {clientname} ยท {currentrole}
-
-
-
-
- {orderid}
-
-
-
-
-
- setTabstatus((e) => (e === 0 ? 1 : 0))}
- sx={{
- display: 'inline-flex',
- alignItems: 'center',
- gap: 0.75,
- px: 1.25,
- py: 0.625,
- borderRadius: 999,
- cursor: 'pointer',
- bgcolor: BRAND,
- color: '#fff',
- fontWeight: 800,
- fontSize: 12,
- boxShadow: `0 6px 18px ${ring(BRAND)}`
- }}
- >
-
-
-
- {currentrole || 'Role'}
-
-
-
-
- {[
- { label: 'Required', value: currentshiftobj.shifts, color: BRAND },
- { label: 'Assigned', value: currentshiftobj.assigned, color: '#10b981' },
- { label: 'Remaining', value: currentshiftobj.remaining, color: '#ef4444' }
- ].map((c) => (
-
- {c.label}
-
- {c.value ?? 0}
-
-
- ))}
-
-
-
-
- {/* title */}
-
- {/*
-
-
-
-
- setTabstatus((e) => (e === 0) ? 1 : 0)}
- variant="scrollable" scrollButtons="auto" >
-
-
-
-
-
-
-
-
-
-
-
-
-
- */}
-
-
- {/* */}
- {/* */}
- {(stafflist.length === 0) ?
- <>
- {(loading) ?
- <>
-
-
-
-
-
-
- >
- :
- No Staffs Available
-
- }
- >
- :
-
-
-
-
-
- #
- Staff
-
- Category
-
- {/* Gender */}
-
- Price
-
- Experience
-
- Level
- City
- Action
-
-
-
-
-
- {stafflist.map((val, i) => {
-
- return
-
-
- res.userid == val.userid)) ? '#f5f5f5' : '', ':hover': {
- backgroundColor: (staffarr.find((res) => res.userid == val.userid)) ? '#f5f5f5 !important' : ''
- }
- }}>
-
- {i + 1}
-
-
-
-
- {/* {row.tenantname.charAt(0)} */}
-
-
- {val.firstname}
-
- {val.contactno}
-
-
-
-
-
- {/*
- {val.contactno}
-
- some demo address
-
- */}
-
- {val.cateoryname}
-
- {/* {row.category} */}
-
-
-
- {val.rolecost}
- {/* */}
- {/* {(row.gender === 1) && } */}
- {/* {row.gender === 0 && } */}
- {/* */}
- {/* */}
- {/* B+ */}
- {/* {row.devicetype} */}
- {val.experience} Years
-
- {val.city}
-
- {(val.orderdetailid !== orderdetailid) ?
- <>
- res.userid == val.userid)) ? true : false}
- onClick={(e) => {
- console.log(currentshiftobj)
- if (currentshiftobj.remaining >= 0) {
-
-
- if (e.target.checked && currentshiftobj.remaining != 0) {
- let arr = staffarr;
- arr.push({
- userid: val.userid,
- orderdetailid,
- productid,
- shiftid: currentshiftobj.shiftid,
- userrate: currentshiftobj.price,
- productrate: val.rolecost,
- firstname: val.firstname
- });
- setStaffarr([...arr])
- let obj = currentshiftobj;
- // obj.shifts--;
- // obj.assigned = arr.length;
- obj.assigned++;
- obj.remaining = obj.shifts - obj.assigned;
- setCurrentshiftobj({ ...obj })
- } else if (currentshiftobj.assigned != currentshiftobj.shifts || (currentshiftobj.remaining === 0 && (!e.target.checked))) {
- let arr = staffarr;
- // let index = arr.indexOf(val.userid)
- let index = arr.findIndex((val1) => val1.userid === val.userid)
- arr.splice(index, 1);
- setStaffarr([...arr]);
- let obj = currentshiftobj;
- // obj.shifts++;
- // obj.assigned = arr.length;
- obj.assigned--;
- obj.remaining = obj.shifts - obj.assigned;
- setCurrentshiftobj({ ...obj })
- }
- // forceUpdate();
- console.log(staffarr);
- }
- }} />
-
-
- >
- :
-
- <>
-
-
- {/* */}
-
- {
- console.log(val)
- unassign(val)
- // sendunassignnotification(val)
- }}>
- {/* */}
-
-
-
-
- {
- console.log(val)
- // unassign(val)
- notificationpush(val)
- }}>
- {/* */}
-
-
-
- >
- }
-
-
-
-
- {(val.orderid) &&
-
- <>
-
- >
-
- }
-
-
-
-
- })
- }
-
-
-
- }
-
-
-
- {(stafflist.length > 0) &&
- <>
- OK
- {
-
- // dialogclose();
- setStaffarr([]);
- let obj = currentshiftobj;
- // obj.shifts = row.orderqty;
- obj.remaining = obj.shifts;
- obj.assigned = 0;
-
- setCurrentshiftobj(obj);
- }}>clear
- >
- }
- { dialogclose() }}>Close
-
-
- {/* */}
- {/* */}
-
-
-
-
-
- {/*
-
-
-
-
-
- */}
- {/* */}
- {/*
- Details
- */}
-
-
-
-
- history.back()}
- sx={{
- bgcolor: '#fff',
- border: `1px solid ${DT.borderSubtle}`,
- borderRadius: 999,
- color: DT.textPrimary,
- '&:hover': { bgcolor: tint(BRAND), borderColor: edge(BRAND), color: BRAND }
- }}
- >
-
-
-
-
-
-
-
- Order Details
-
-
-
-
- {orderid === '' ? : orderid}
-
-
-
- {orderdate === '' ? : orderdate}
-
-
-
-
-
-
-
- {((orderstatus === 'pending') ||
- (orderstatus === 'assigned') ||
- (orderstatus === 'confirmed') ||
- (orderstatus === 'modified')) && (
-
- }
- onClick={(e) => {
- e.stopPropagation();
- if (dayjs(dayjs().format('MM-DD-YYYY')).isBefore(dayjs(dayjs(startdate).format('MM-DD-YYYY')))) {
- navigate(`/editorder`, {
- state: {
- orderheaderid: orderheaderid,
- tenantid: tenantid
- }
- });
- } else {
- enqueueSnackbar('Order cannot be edited.\n Order date is not valid at this time', {
- variant: 'error',
- anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 4000,
- style: { whiteSpace: 'pre-line' }
- });
- }
- }}
- >
- Edit Order
-
-
- )}
-
- {orderstatus !== 'cancelled' && orderstatus !== '' && orderstatus !== 'completed' && orderstatus !== 'closed' && (
- {
- if ((dayjs(startdate).diff(dayjs(), 'm') / 60) > 24) {
- setInvoiceeligible(false);
- setOpen(true);
- } else {
- setInvoiceeligible(true);
- setOpen(true);
- }
- }}
- startIcon={}
- >
- Cancel Order
-
- )}
-
- {orderstatus === 'cancelled' && (
-
- Cancelled on {cancelleddate}
-
- )}
-
-
-
-
-
- {/* Dialog window */}
-
-
-
-
- {/* {(dayjs().isBefore(dayjs(startdate)))?'true':'false'}
*/}
-
- {/* */}
- {/* */}
- {/* */}
- {/*
-
- BEO
-
-
-
- */}
- {/* */}
-
- {/*
-
- Due Date
-
- {duedate}
- */}
- {/*
- */}
- {/* */}
- {/* */}
-
-
-
-
- Client
- {(clientname === '') ? : clientname}
-
- {/* {eventlocation.map((val, i) => {
- return {val}
- })
-
- }
- {eventlocation}
- fafdf
- dafaf
- afdafafd */}
- {(tenantaddress === '') ? : tenantaddress}
-
-
-
-
-
-
-
-
- Event
-
- {(eventname === '') ? : eventname}
- {/*
- {(eventlocation === '') ? : eventlocation}
-
- : venuetype} color="primary" variant="light" size="small" sx={{ width: 'max-content' }} />
- */}
-
-
-
-
-
-
- {categoryarr.map((val5, j) => {
-
-
-
-
-
- return < Fragment key={val5.locationaddress}>
-
-
-
-
-
- Shift {j + 1}
-
-
- {''}{val5.locationaddress}
-
- {(val5.shiftstatus === 1) &&
-
- }
-
-
- {val5.ordercontacts.map((val11) => {
-
- return <>
-
- {val11.contactname.charAt(0).toUpperCase()}
-
- >
- })
-
- }
-
-
-
-
-
-
-
-
- #
- Role
- Start Date
- End Date
- Unpaid break
- Count
- Assigned
- Attire
- Price
- {/* Tax */}
- Amount
- Action
-
-
-
- {(val5.orderdetails.length === 0) &&
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/*
-
-
- */}
-
-
- {/* */}
- >
- }
-
- {/* */}
- {val5.orderdetails.map((row, i) => (
- <>
-
- {i + 1}
- {row.productname}
- {/* {row.productname}
- */}
-
-
- {dayjs(row.starttime).format('MM/DD/YYYY')}
- {dayjs(row.starttime).format('hh:mm A')}
-
-
-
- {' '}
-
- {dayjs(row.endtime).format('MM/DD/YYYY')}
- {dayjs(row.endtime).format('hh:mm A')}
-
-
- {row.unpaidbreak || 0}
-
-
-
-
-
-
-
-
-
- {/* < Grid container spacing={1}>
- {(row.orderattires || []).map((val) => {
- return
-
-
-
-
- })
-
-
- }
- */}
-
- ${row.price}
- {/* {row.taxamount} */}
- ${row.landingamount}
-
-
-
-
-
-
- {
- setStafflist([]);
- setExpandopen(((expandopen[0] === j) && (expandopen[1] === i)) ? ['', ''] : [j, i])
- // expanddatafetch(row.orderheaderid);
-
- // fetchroleslist(row.productid, '', '', val5.orderheaderid, row.shiftid);
- fetchstafflist(row.orderdetailid)
-
-
- }
- }
- >
- {((expandopen[0] === j) && (expandopen[1] === i)) ?
-
- :
- }
-
-
-
-
-
- {/* {(orderstatus !== 'cancelled') &&
- <>
-
- {
- setExpandopen(['', ''])
- setCurrentrole(row.productname);
- setStarttime(row.starttime);
- console.log('row')
-
- console.log(row)
- setEndtime(row.endtime);
- setProductid(row.productid);
- setOrderdetailid(row.orderdetailid);
- if ((row.orderqty === row.supplyqty)
- || (row.status == 1)
- || (val5.shiftstatus == 1)
-
- ) {
- fetchroleslist(row.productid, '', '', val5.orderheaderid, row.shiftid);
-
- } else {
- fetchroleslist(row.productid, row.starttime, row.endtime, val5.orderheaderid, row.shiftid);
-
- }
-
- setStaffarr([]);
-
-
- let obj = currentshiftobj;
- obj.shifts = row.orderqty;
-
- obj.shiftid = row.shiftid;
- obj.remaining = row.orderqty - row.supplyqty;
- obj.assigned = row.supplyqty;
- obj.price = row.price;
-
- setCurrentshiftobj(obj);
-
- setDialogopen(true);
- }} >
-
-
-
- >
- } */}
- {(orderstatus === 'cancelled') &&
- <>
-
- >
- }
-
- {(row.status === 1) &&
-
-
-
-
-
- }
- {(row.supplyqty > row.orderqty) &&
-
-
-
-
-
- }
-
-
-
-
-
-
-
-
-
-
-
- {/* */}
-
-
- {(stafflist.length === 0) ?
- <>
- {(loading) ?
- <>
-
-
-
-
-
-
- >
- :
- <>
-
- No Staffs has been Assigned
-
- >
- }
- >
- :
-
-
-
-
-
- #
- Staff
- Start Time
- End Time
- Pay Rate
-
- {/* Category */}
- Clockin
- Clockout
-
- Hours Worked
-
-
-
-
- {/* Experience */}
-
- {/* Level */}
- {/* City */}
- Status
-
-
-
-
- {stafflist.map((val, i) => {
-
- return
-
-
- res.userid == val.userid)) ? '#f5f5f5' : '', ':hover': {
- // backgroundColor: (staffarr.find((res) => res.userid == val.userid)) ? '#f5f5f5 !important' : ''
- // }
-
- }}>
-
- {i + 1}
-
- {/*
-
-
-
-
-
- {val.staffname}
-
-
-
-
- */}
-
-
-
- {val.staffname}
-
-
-
-
-
-
-
-
- {dayjs(val.Starttime).format('MM/DD/YYYY')}
- {dayjs(val.Starttime).format('hh:mm A')}
-
-
-
-
- {dayjs(val.Endtime).format('MM/DD/YYYY')}
- {dayjs(val.Endtime).format('hh:mm A')}
-
-
- {val.rolecost}
-
- {/*
-
- {val.cateoryname}
-
-
-
- */}
-
-
-
- {/* Clock In: */}
- {/* Age */}
-
-
-
-
-
-
-
- {/* Clock In: */}
- {/* Age */}
-
-
-
-
-
-
- {val.hoursworked}
-
-
- {/*
-
-
-
-
-
-
-
-
- {val.tenantname}
-
-
-
-
-
- {val.orderid}
-
-
- {dayjs(val.startdate).utc().format('MM/DD/YYYY')}
-
-
-
-
-
- {val.eventname}
-
- {`${val.locationaddress.slice(0, 15)}...`}
-
-
- */}
-
-
-
-
-
-
- {(val.orderstatus === 'pending') &&
-
-
- }
- {(val.orderstatus === 'cancelled') &&
-
-
-
- }
- {(val.orderstatus === 'completed') &&
-
-
- }
- {(val.orderstatus === 'processing') &&
-
- }
- {(val.orderstatus === 'assigned') &&
-
- }
- {(val.orderstatus === 'confirmed') &&
-
- }
-
- {(val.orderstatus === 'active') &&
-
- }
- {(val.orderstatus === 'closed') &&
-
- }
-
-
-
-
-
-
- })
- }
-
-
-
- }
-
-
-
-
-
-
-
-
-
-
- >
- ))}
-
-
-
-
-
-
-
-
-
- })
- }
-
-
-
-
-
-
- {/* */}
- {/* Order Addons
- */}
-
- {
-
-
- (orderaddons || []).map((val) => {
- return
-
- } variant='outlined' color="error" label={val.addon} />
-
-
- })
- }
- {/*
- } variant='outlined' color="error" label="Parking Provided" />
-
-
- } variant='outlined' color="error" label="Meal Provided" />
- */}
- {/*
- } color="error" label="Like" />
-
-
- } color="error" label="Like" />
-
-
- } color="error" label="Like" />
- */}
-
- {/* */}
-
-
- {/* */}
-
- {/* */}
-
-
-
- Sub Total:
- ${(subtotal === '') ? : subtotal}
-
- {/*
- Discount:
-
- 0
-
- */}
-
- Tax:
- {(taxamount === '') ? : taxamount}
-
-
- Grand Total:
- {(grandtotal === '') ? : `$${grandtotal}`}
-
-
-
- {/* */}
-
-
-
- Other Instructions:
-
-
- {otherinstructions}
-
-
-
- {/*
- }>Send Notifications
- */}
-
-
-
- {/* */}
-
- {/* { notificationpush() }}>notify */}
-
-
- >
- );
-};
-
-export default Details;
diff --git a/src/pages/nearle/orders/multipleOrders.js b/src/pages/nearle/orders/multipleOrders.js
index b045285..b3ab2ae 100644
--- a/src/pages/nearle/orders/multipleOrders.js
+++ b/src/pages/nearle/orders/multipleOrders.js
@@ -1,7 +1,8 @@
/* eslint-disable no-unused-vars */
-import React, { useEffect, useState, useRef } from 'react';
-import { getAdminPricing, getTenantLocations, getTenantCustomers, createExpressBookingBulk } from 'pages/api/doormileApi';
+import React, { useEffect, useMemo, useState, useRef } from 'react';
+import { getAdminPricing, getAdminTenants, getTenantLocations, getAdminCustomers, createExpressBookingBulk } from 'pages/api/doormileApi';
import { fetchAppLocations as fetchZoneLocations } from 'pages/api/api';
+import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
import Papa from 'papaparse';
import * as XLSX from 'xlsx';
import dayjs from 'dayjs';
@@ -74,14 +75,48 @@ dayjs.extend(utc);
const cellHeaderSx = { fontSize: 11.5, fontWeight: 700, color: '#475569', py: 0.75, px: 1 };
const cellBodySx = { fontSize: 12, py: 0.6, px: 1 };
+// GET /admin/tenants/:id/locations carries no `slots` field (no per-location
+// pickup-hours endpoint in the new API) โ the pickup-slot Autocomplete used
+// to read tenantLocation.slots, which was always undefined, leaving the
+// dropdown permanently empty and the page unsubmittable. Falls back to a
+// fixed 09:00-21:00 / 30-min window, same rationale as createorder1.js's
+// fetchTiming().
+const buildDefaultPickupSlots = () => {
+ const arr = [];
+ for (let t = dayjs().hour(9).minute(0).second(0); t.hour() < 21 || (t.hour() === 21 && t.minute() === 0); t = t.add(30, 'm')) {
+ arr.push({ name: t.format('hh:mm A'), time: t.format('HH:mm') });
+ }
+ return arr;
+};
+
+// Some saved locations come back with a blank locationname (live-confirmed,
+// e.g. tenant 1's only location) โ fall back to the address rather than
+// showing an empty field/option.
+const locationDisplayName = (loc) => (loc?.locationname && loc.locationname.trim()) || loc?.address || 'Unnamed location';
+
+// Tenants with many locations in the same city (DailyGrubs has 23, all in
+// Coimbatore) made a "(city)" suffix useless for telling them apart โ sort
+// primary-first then alphabetically; the full address is shown per-option
+// instead (see renderOption on the Business Location Autocomplete below).
+const sortLocations = (locations) =>
+ [...(locations || [])].sort((a, b) => {
+ if (!!a.isprimary !== !!b.isprimary) return a.isprimary ? -1 : 1;
+ return locationDisplayName(a).localeCompare(locationDisplayName(b));
+ });
+
const MultipleOrders = () => {
const navigate = useNavigate();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const locationRef = useRef(null);
const userid = localStorage.getItem('userid');
- // tenantid is fixed for this app โ comes from the logged-in session
- const tenantid = localStorage.getItem('tenantid');
+ // Per express-console-api.md's "Tenant scoping" section: token tenantid
+ // 0/null means Doormile's own staff, who can act on any tenant โ NOT an
+ // error state. A client (tenant) login carries a real tenantid and never
+ // needs the picker below. `tenantid` (used throughout this file already)
+ // now resolves to whichever the operator is currently working as.
+ const loggedInTenantId = localStorage.getItem('tenantid');
+ const isStaffLogin = !loggedInTenantId || loggedInTenantId === '0';
// ============================== state ==============================
const [locations, setLocations] = useState([]);
@@ -92,6 +127,11 @@ const MultipleOrders = () => {
const [appId, setAppId] = useState(0);
const [locationid, setLocationid] = useState(0);
const [locationValue, setLocationValue] = useState(null);
+ const [locationsStatus, setLocationsStatus] = useState('loading');
+ const [selectedTenantId, setSelectedTenantId] = useState(isStaffLogin ? '' : loggedInTenantId);
+ const [staffTenantList, setStaffTenantList] = useState([]);
+ const [staffTenantListLoading, setStaffTenantListLoading] = useState(false);
+ const tenantid = selectedTenantId;
const [basePrice, setBasePrice] = useState(0);
const [pricePerKm, setPricePerKm] = useState(0);
@@ -105,7 +145,7 @@ const MultipleOrders = () => {
const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY'));
const [selectedtime, setSelectedtime] = useState('');
- const [pickupSlotsList, setPickupSlotsList] = useState(null);
+ const [pickupSlotsList, setPickupSlotsList] = useState(buildDefaultPickupSlots);
const [pickupSlot, setPickupSlot] = useState(null);
const [otherinstructions, setOtherinstructions] = useState('');
@@ -148,7 +188,7 @@ const MultipleOrders = () => {
setUsers([]);
setUploadType(null);
setFileName('');
- setPickupSlotsList(null);
+ setPickupSlotsList(buildDefaultPickupSlots());
setPickupSlot(null);
setSelectedtime('');
}, [appId]);
@@ -174,68 +214,104 @@ const MultipleOrders = () => {
const fetchTenantPricing = async (id) => {
try {
const pricing = (await getAdminPricing()) || [];
- const match = pricing.find((p) => p.tenantid === id);
+ // id comes from localStorage (always a string) but pricing.tenantid is
+ // numeric โ a strict === here never matched, so basePrice/pricePerKm/
+ // minKm stayed at their 0 defaults and every order priced at โน0.
+ const match = pricing.find((p) => String(p.tenantid) === String(id));
if (match) {
setBasePrice(match.baseprice || 0);
setPricePerKm(match.priceperkm || 0);
setMinKm(match.basedistance || 0);
}
} catch (err) {
- console.log('fetchTenantPricing', err);
+ // swallow
}
};
useEffect(() => {
if (tenantid) fetchTenantPricing(tenantid);
- }, []);
+ }, [tenantid]);
// ============================== gettenantlocations ==============================
+ // Drives what shows in place of the Business Location picker. Previously
+ // this function was only ever called when `tenantid` was truthy, so an
+ // operator on a no-tenant login (the shared admin/developer account has
+ // tenantid: null) got a permanently empty picker with zero feedback โ not
+ // even a toast, since the function body that would show one never ran.
const gettenantlocations = async (id) => {
+ if (!id) {
+ // Staff login with no tenant picked yet โ the picker UI covers this.
+ setLocationsStatus('no-tenant');
+ return;
+ }
+ setLocationsStatus('loading');
try {
const details = (await getTenantLocations(id)) || [];
+ setTenantlocations(details);
if (details.length === 1) {
- setTenantlocations(details);
+ setLocationsStatus('ready');
setPickCust(details[0]);
- setLocationid(details[0].locationid);
+ setLocationid(details[0].tenantlocationid);
setLocationValue(details[0]);
- setPickupSlotsList(details[0].slots);
+ } else if (details.length === 0) {
+ setLocationsStatus('empty');
+ opentoast('No saved business locations for this tenant', 'info', 3000);
} else {
- setTenantlocations(details);
+ setLocationsStatus('ready');
}
} catch (err) {
- console.log('gettenantlocations', err);
+ // doormileAxios's response interceptor rejects with error.response.data
+ // (not the raw axios error), so the message lives at err.message here.
+ setTenantlocations([]);
+ setLocationsStatus('error');
+ opentoast(err?.message || 'Could not load business locations', 'error', 4000);
}
};
useEffect(() => {
- if (tenantid) gettenantlocations(tenantid);
+ gettenantlocations(tenantid);
+ }, [tenantid]);
+
+ // ============================== fetchStaffTenantList (Doormile staff picker) ==============================
+ // Only staff logins (loggedInTenantId blank/0) need this โ a client login's
+ // tenant is already fixed by their token and this never fetches for them.
+ useEffect(() => {
+ if (!isStaffLogin) return;
+ setStaffTenantListLoading(true);
+ getAdminTenants()
+ .then((tenants) => setStaffTenantList((tenants || []).filter((t) => t.status === 'Active')))
+ .catch((err) => {
+ opentoast(err?.message || 'Could not load tenant list', 'error', 4000);
+ })
+ .finally(() => setStaffTenantListLoading(false));
}, []);
// ============================== clientdetails ==============================
+ // GET /admin/customers (B2C directory) โ not /admin/tenantcustomers, which
+ // is an empty, manually-added contact book with no address/coordinates.
+ // This resource auto-grows from real orders (see the identical fix on
+ // createorder1.js's Saved Contacts) and has a confirmed live shape of
+ // { appcustomerid, createdat, email, name, phone, totalbookings } โ no
+ // search/query param exists, so it's fetched once and filtered in-memory.
const clientdetails = async () => {
try {
- const customers = (await getTenantCustomers()) || [];
- const filtered = searchCustList
- ? customers.filter(
- (val) =>
- val.firstname?.toLowerCase().includes(searchCustList.toLowerCase()) ||
- val.phone?.includes(searchCustList) ||
- val.contactno?.includes(searchCustList)
- )
- : customers;
- setCustomerlist(filtered);
+ const customers = (await getAdminCustomers()) || [];
+ setCustomerlist(customers);
} catch (err) {
opentoast('server error', 'warning');
}
};
useEffect(() => {
- if (!tenantid) return;
- const t = setTimeout(() => {
- if (searchCustList === '' || searchCustList.length > 2) clientdetails();
- }, 250);
- return () => clearTimeout(t);
- }, [searchCustList, tenantid]);
+ if (!tenantid || !isCustomerOpen) return;
+ clientdetails();
+ }, [tenantid, isCustomerOpen]);
+
+ const filteredCustomerList = useMemo(() => {
+ const q = searchCustList.trim().toLowerCase();
+ if (!q) return customerlist;
+ return customerlist.filter((c) => c.name?.toLowerCase().includes(q) || c.phone?.includes(searchCustList));
+ }, [customerlist, searchCustList]);
// ============================== totals ==============================
useEffect(() => {
@@ -276,27 +352,51 @@ const MultipleOrders = () => {
};
// ============================== checkbox handlers ==============================
- const handleCheckboxChange = async (event, customer) => {
- setLoading(true);
- try {
- if (event.target.checked) {
- const { roundedDistance, totalcharge } = await calculateDistance(customer);
- setDropCust((prev) => [...prev, { ...customer, distance: roundedDistance, totalcharge }]);
- } else {
- setDropCust((prev) => prev.filter((c) => c.customerid !== customer.customerid));
- }
- } catch (err) {
- console.error('Failed to calculate distance:', err);
- } finally {
- setLoading(false);
+ // GET /admin/customers has no address/coordinates at all, so a saved
+ // customer can no longer be distance-calculated at selection time (that's
+ // what used to throw "Invalid coordinates" and silently drop the row every
+ // time). Add the row immediately instead โ its Address cell renders an
+ // editable AddressAutocomplete (see the Drop List table) until the
+ // operator picks a real address, which is what fills in lat/lng and lets
+ // distance/charge be computed per-row.
+ const handleCheckboxChange = (event, customer) => {
+ const rowkey = `sel-${customer.appcustomerid}`;
+ if (event.target.checked) {
+ setDropCust((prev) => {
+ if (prev.some((c) => c._rowkey === rowkey)) return prev;
+ return [
+ ...prev,
+ {
+ _rowkey: rowkey,
+ appcustomerid: customer.appcustomerid,
+ firstname: customer.name || '',
+ contactno: customer.phone || '',
+ address: '',
+ city: '',
+ postcode: '',
+ latitude: null,
+ longitude: null,
+ distance: 0,
+ totalcharge: 0,
+ quantity: 0,
+ collectionamt: 0
+ }
+ ];
+ });
+ } else {
+ setDropCust((prev) => prev.filter((c) => c._rowkey !== rowkey));
}
};
+ // Still used by the CSV "Continue" promotion and the Drop List remove
+ // button. Safe to keep calling calculateDistance here โ CSV rows always
+ // carry real latitude/longitude from the sheet, unlike Selection rows.
const handleCheckboxChange1 = async (customer) => {
+ const rowkey = customer._rowkey;
let wasSelected = false;
setDropCust((prev) => {
- wasSelected = prev.some((c) => c.firstname === customer.firstname);
- if (wasSelected) return prev.filter((c) => c.firstname !== customer.firstname);
+ wasSelected = prev.some((c) => c._rowkey === rowkey);
+ if (wasSelected) return prev.filter((c) => c._rowkey !== rowkey);
return prev;
});
if (wasSelected) return;
@@ -304,16 +404,55 @@ const MultipleOrders = () => {
try {
const { roundedDistance, totalcharge } = await calculateDistance(customer);
setDropCust((prev) => {
- if (prev.some((c) => c.firstname === customer.firstname)) return prev;
+ if (prev.some((c) => c._rowkey === rowkey)) return prev;
return [...prev, { ...customer, distance: roundedDistance, totalcharge }];
});
} catch (err) {
console.error('Failed to calculate distance:', err);
+ OpenToast(`Could not calculate distance for ${customer.firstname || 'this row'}`, 'warning', 3000);
} finally {
setLoading(false);
}
};
+ // Parses the AddressAutocomplete `place` result (see createorder1.js's
+ // handleDropPlaceSelected for the same pattern) for one Drop List row,
+ // updates that row's address/coordinates, then recomputes its distance and
+ // charge now that coordinates finally exist.
+ const handleRowPlaceSelected = async (rowkey, place) => {
+ const parsed = { suburb: '', city: '', postcode: '' };
+ place.address_components.forEach((component) => {
+ component.types.forEach((type) => {
+ switch (type) {
+ case 'sublocality_level_1':
+ case 'sublocality':
+ parsed.suburb = component.long_name;
+ break;
+ case 'locality':
+ parsed.city = component.long_name;
+ break;
+ case 'postal_code':
+ parsed.postcode = component.long_name;
+ break;
+ }
+ });
+ });
+ const latitude = place.geometry.location.lat();
+ const longitude = place.geometry.location.lng();
+ const address = place.formatted_address || '';
+
+ setDropCust((prev) =>
+ prev.map((c) => (c._rowkey === rowkey ? { ...c, address, city: parsed.city, postcode: parsed.postcode, latitude, longitude } : c))
+ );
+
+ try {
+ const { roundedDistance, totalcharge } = await calculateDistance({ latitude, longitude });
+ setDropCust((prev) => prev.map((c) => (c._rowkey === rowkey ? { ...c, distance: roundedDistance, totalcharge } : c)));
+ } catch (err) {
+ console.error('Failed to calculate distance:', err);
+ }
+ };
+
// No zone-admin-token or bulk-notify endpoint in the new API.
const fetchAppAdminTokens = async () => setAdmintoken([]);
@@ -337,6 +476,12 @@ const MultipleOrders = () => {
receiverphone: 'contactno',
'receiveralternatephone*': 'altcontactno',
receiverfulladdress: 'address',
+ // Best-effort โ no live sample sheet to confirm these are the tenant's
+ // actual column headers. Without them deliverycity/deliverypincode in
+ // the submit payload were always sent blank; if these names are wrong,
+ // behavior is unchanged from before (still blank), not worse.
+ receivercity: 'city',
+ receiverpincode: 'postcode',
receiverlatitude: 'latitude',
receiverlongitude: 'longitude',
'itemdescription*': 'description',
@@ -362,8 +507,8 @@ const MultipleOrders = () => {
const processData = (data, headers) => {
const normalizedMap = {};
for (const key in headerMap) normalizedMap[normalizeHeader(key)] = headerMap[key];
- const mappedData = data.map((row) => {
- const newRow = {};
+ const mappedData = data.map((row, idx) => {
+ const newRow = { _rowkey: `csv-${idx}` };
for (const key in row) {
const cleanKey = normalizeHeader(key);
const newKey = normalizedMap[cleanKey] || cleanKey;
@@ -432,11 +577,11 @@ const MultipleOrders = () => {
};
// ============================== row editors ==============================
- const handleQuantityChange = (customerid, value) => {
- setDropCust((prev) => prev.map((c) => (c.customerid === customerid ? { ...c, quantity: Number(value) || 0 } : c)));
+ const handleQuantityChange = (rowkey, value) => {
+ setDropCust((prev) => prev.map((c) => (c._rowkey === rowkey ? { ...c, quantity: Number(value) || 0 } : c)));
};
- const handleCollectionAmtChange = (customerid, value) => {
- setDropCust((prev) => prev.map((c) => (c.customerid === customerid ? { ...c, collectionamt: Number(value) || 0 } : c)));
+ const handleCollectionAmtChange = (rowkey, value) => {
+ setDropCust((prev) => prev.map((c) => (c._rowkey === rowkey ? { ...c, collectionamt: Number(value) || 0 } : c)));
};
// ============================== buildDeliveryTime ==============================
@@ -470,30 +615,39 @@ const MultipleOrders = () => {
opentoast('Add at least one drop customer', 'warning');
return;
}
+ // A Selection-sourced row (picked from Saved Customers) starts with no
+ // address/coordinates until the operator fills in its Address cell โ
+ // block submit rather than send a booking with lat/lng silently at 0.
+ const incomplete = dropCust.find((c) => c.latitude == null || c.longitude == null);
+ if (incomplete) {
+ opentoast(`Add a delivery address for ${incomplete.firstname || 'a selected customer'} before submitting`, 'warning', 4000);
+ return;
+ }
const deliverytime = buildDeliveryTime();
// POST /admin/expressbooking/bulk takes { bookings: [...] } where each
- // entry is a flat booking object (tenantid, pickuplocationid, customer_*,
+ // entry is a flat booking object (tenantid, pickup* fields, customer_*,
// delivery*, service_option, finalprice, notes, parcels[]) โ not the old
- // per-row orders/pickup/drop shape. pickup* fields follow the doc's own
- // delivery*/pickup* naming symmetry (unverified โ not shown in the doc's
- // example, only pickuplocationid is confirmed).
+ // per-row orders/pickup/drop shape.
// tenantid comes from the logged-in session (see the top-level `tenantid`
// const above), not from pickCust โ pickCust is a tenant *location*
// object (GET /admin/tenants/:id/locations) and was never confirmed to
// carry a tenantid back-reference, so `pickCust.tenantid` was silently
// undefined on every submit.
+ //
+ // Always send raw pickup* fields, never pickuplocationid โ live-confirmed
+ // pickuplocationid currently 500s on POST /admin/expressbooking (and by
+ // extension the bulk endpoint, same handler); a saved location's own
+ // address/pincode/city/lat/lng (already on pickCust) work fine sent raw.
+ // Note: pickCust.pincode, not .postcode โ that was also wrong (the real
+ // GET /admin/tenants/:id/locations field is `pincode`).
const bookings = dropCust.map((customer) => ({
tenantid: Number(tenantid),
- ...(pickCust.locationid
- ? { pickuplocationid: pickCust.locationid }
- : {
- pickupaddress: pickCust.address || '',
- pickuppincode: pickCust.postcode || '',
- pickupcity: pickCust.city || '',
- pickuplatitude: Number(pickCust.latitude) || 0,
- pickuplongitude: Number(pickCust.longitude) || 0
- }),
+ pickupaddress: pickCust.address || '',
+ pickuppincode: pickCust.pincode || '',
+ pickupcity: pickCust.city || '',
+ pickuplatitude: Number(pickCust.latitude) || 0,
+ pickuplongitude: Number(pickCust.longitude) || 0,
customer_phone: customer.contactno?.toString() || '',
customer_name: customer.firstname || '',
deliveryaddress: customer.address || '',
@@ -514,17 +668,16 @@ const MultipleOrders = () => {
const res = await createExpressBookingBulk(bookings);
if (res.success) {
opentoast('Orders Created Successfully', 'success', 2000);
- navigate('/nearle/orders');
+ navigate('/doormile/orders');
} else {
opentoast(res?.message || 'Order creation failed. Please try again.', 'warning', 3000);
}
} catch (err) {
- const msg = err.response
- ? err.response.data?.message || `Server error (${err.response.status})`
- : err.request
- ? 'Network error. Check your internet connection.'
- : 'Something went wrong.';
- opentoast(msg, 'error', 3000);
+ // doormileAxios's response interceptor rejects with error.response.data
+ // (not the raw axios error), so the message lives at err.message here โ
+ // err.response was always undefined at this point, so this always fell
+ // through to the generic "Something went wrong." fallback before.
+ opentoast(err?.message || 'Something went wrong.', 'error', 3000);
} finally {
setLoading(false);
setBtnLoading(false);
@@ -605,13 +758,108 @@ const MultipleOrders = () => {
- {tenantLocations?.length === 1 ? (
+ {locationsStatus === 'loading' ? (
+
+ Loading business locationsโฆ
+
+ ) : locationsStatus === 'no-tenant' && isStaffLogin ? (
+ // Staff login (express-console-api.md: token tenantid
+ // 0/null = "Doormile's own staff... sees everything, all
+ // tenants") โ pick which tenant this bulk order is for.
+ option?.tenantname || ''}
+ isOptionEqualToValue={(option, value) => option?.tenantid === value?.tenantid}
+ onChange={(event, value) => setSelectedTenantId(value ? String(value.tenantid) : '')}
+ renderInput={(params) => (
+
+ )}
+ />
+ ) : locationsStatus === 'no-tenant' ? (
+
+ This login has no tenant assigned โ sign in as a tenant user
+
+ ) : locationsStatus === 'error' ? (
+ gettenantlocations(tenantid)}
+ sx={{
+ display: 'flex',
+ alignItems: 'center',
+ px: 1.5,
+ py: 1,
+ borderRadius: '10px',
+ bgcolor: '#fef2f2',
+ border: '1px solid #fecaca',
+ color: '#b91c1c',
+ fontWeight: 600,
+ fontSize: 12.5,
+ cursor: 'pointer'
+ }}
+ >
+ Could not load business locations โ tap to retry
+
+ ) : locationsStatus === 'empty' ? (
+
+ No saved business locations for this tenant
+
+ ) : tenantLocations?.length === 1 ? (
{
- option ? `${option.locationname} (${option.suburb || option.locationsuburb || ''})` : ''
- }
+ options={sortLocations(tenantLocations)}
+ getOptionLabel={(option) => (option ? locationDisplayName(option) : '')}
+ renderOption={(props, option) => (
+
+
+
+
+ {locationDisplayName(option)}
+
+ {option.isprimary && (
+
+ PRIMARY
+
+ )}
+
+
+ {option.address || option.city || ''}
+
+
+
+ )}
value={locationValue}
onChange={(event, value, reason) => {
if (reason === 'clear' || !value) {
setLocationid(0);
setLocationValue(null);
setPickCust(null);
- setPickupSlotsList(null);
} else {
- setLocationid(value.locationid || 0);
+ setLocationid(value.tenantlocationid || 0);
setLocationValue(value);
setPickCust(value);
- setPickupSlotsList(value?.slots || null);
}
}}
renderInput={(params) => (
@@ -746,7 +1020,7 @@ const MultipleOrders = () => {
const finalDateTime = dayjs(`${startdate} ${formattedTime}`, 'MM-DD-YYYY hh:mm A').format('YYYY-MM-DD hh:mm A');
setPickupSlot(finalDateTime);
}}
- getOptionLabel={(option) => (option ? `${option.name} (${dayjs(option.time, 'HH:mm').format('hh:mm A')})` : '')}
+ getOptionLabel={(option) => (option ? option.name : '')}
renderInput={(params) => (
{
{dropCust.map((customer, index) => (
-
+
{index + 1}
{customer.firstname}
-
-
-
- {customer.address}
-
-
+
+ {customer.latitude == null || customer.longitude == null ? (
+
+ setDropCust((prev) => prev.map((c) => (c._rowkey === customer._rowkey ? { ...c, address: text } : c)))
+ }
+ onPlaceSelected={(place) => handleRowPlaceSelected(customer._rowkey, place)}
+ bias={pickCust?.latitude ? { lat: pickCust.latitude, lng: pickCust.longitude } : undefined}
+ textFieldSx={{ '& .MuiOutlinedInput-root': { height: 30, fontSize: 12, borderRadius: '8px' } }}
+ />
+ ) : (
+
+
+ {customer.address}
+
+
+ )}
{uploadType === 0 ? (
@@ -1212,7 +1500,7 @@ const MultipleOrders = () => {
size="small"
type="number"
value={customer.quantity || ''}
- onChange={(e) => handleQuantityChange(customer.customerid, e.target.value)}
+ onChange={(e) => handleQuantityChange(customer._rowkey, e.target.value)}
inputProps={{ min: 0 }}
sx={{ width: 64, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }}
/>
@@ -1229,7 +1517,7 @@ const MultipleOrders = () => {
placeholder="0"
onChange={(e) => {
const v = Number(e.target.value);
- handleCollectionAmtChange(customer.customerid, v > 0 ? v : 0);
+ handleCollectionAmtChange(customer._rowkey, v > 0 ? v : 0);
}}
inputProps={{ min: 0 }}
InputProps={{ startAdornment: โน }}
@@ -1237,7 +1525,7 @@ const MultipleOrders = () => {
/>
)}
- {customer.distance}
+ {customer.distance || 'โ'}
โน{Number(customer?.totalcharge || 0).toFixed(2)}
@@ -1477,7 +1765,7 @@ const MultipleOrders = () => {
autoFocus
fullWidth
id="input-search-header"
- placeholder="Search by name, contact, or addressโฆ"
+ placeholder="Search by name or phoneโฆ"
value={searchCustList}
onChange={(e) => setSearchCustList(e.target.value)}
sx={{ bgcolor: 'white', borderRadius: '10px', '& .MuiOutlinedInput-input': { p: '10px 14px' } }}
@@ -1498,25 +1786,28 @@ const MultipleOrders = () => {
- {customerlist?.length === 0 ? (
+ {filteredCustomerList?.length === 0 ? (
) : (
- {customerlist?.map((customer, index) => {
- const checked = dropCust.some((c) => c.customerid === customer.customerid);
+ {filteredCustomerList?.map((customer, index) => {
+ const rowkey = `sel-${customer.appcustomerid}`;
+ const checked = dropCust.some((c) => c._rowkey === rowkey);
return (
-
+
handleCheckboxChange(event, customer)} />}
label={
- {customer.firstname} ({customer.contactno})
+ {customer.name} ({customer.phone})
+
+
+ Address not on file โ add one after selecting
- {customer.address}
}
/>
diff --git a/src/pages/nearle/orders/optimisedOrderPreview.js b/src/pages/nearle/orders/optimisedOrderPreview.js
index 2071f6f..214943e 100644
--- a/src/pages/nearle/orders/optimisedOrderPreview.js
+++ b/src/pages/nearle/orders/optimisedOrderPreview.js
@@ -135,7 +135,6 @@ const OptimisedOrderPreview = () => {
});
setFinalDeliveryList(updateDeliveryAmtList);
- console.log('finaldeliveryList', updateDeliveryAmtList);
}, [deliverylist]);
// ==============================|| fetchPaymentType ||============================== //
@@ -187,10 +186,19 @@ const OptimisedOrderPreview = () => {
const createFinalDeliveryMutation = useMutation({
mutationFn: finalCreatedeliveries, // for optimised delivery create
- onSuccess: (data, variables) => {
- console.log('data', data);
- console.log('varialbles', variables);
- notifyRiderMutation.mutate(rider.userfcmtoken || riderToken); // Call notifyRider after success
+ onSuccess: (data) => {
+ // rider state here is never set (no setRider call anywhere in this
+ // file), so rider.userfcmtoken threw on every success โ also the
+ // wrong argument type regardless (notifyRider needs a milerprofileid,
+ // not an FCM token; see dispatch/Preview.js's identical fix). Notify
+ // every rider actually present in the committed deliveries instead.
+ const notifiedRiderIds = new Set();
+ (finaldeliveryList || []).forEach((order) => {
+ const riderId = order.userid;
+ if (riderId == null || notifiedRiderIds.has(String(riderId))) return;
+ notifiedRiderIds.add(String(riderId));
+ notifyRiderMutation.mutate(riderId);
+ });
if (data.status == 'accepted') {
OpenToast('Delivery Created Successfully', 'success', 2000);
}
diff --git a/src/pages/nearle/orders/orders.js b/src/pages/nearle/orders/orders.js
index 52d48e2..fb38cbd 100644
--- a/src/pages/nearle/orders/orders.js
+++ b/src/pages/nearle/orders/orders.js
@@ -1,7 +1,7 @@
/* eslint-disable no-unused-vars */
import * as React from 'react';
import { enqueueSnackbar } from 'notistack';
-import { useState, useEffect, Fragment, useRef } from 'react';
+import { useState, useEffect, useMemo, useCallback, Fragment, useRef } from 'react';
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
@@ -29,7 +29,10 @@ import {
CircularProgress,
InputBase,
Backdrop,
- TableContainer
+ TableContainer,
+ Divider,
+ Chip,
+ Checkbox
} from '@mui/material';
import {
MdAccessTime,
@@ -48,16 +51,18 @@ import {
MdReceiptLong,
MdClear,
MdNotes,
- MdMyLocation
+ MdMyLocation,
+ MdOutlineFlag,
+ MdPerson
} from 'react-icons/md';
import { DeleteOutlined } from '@ant-design/icons';
import Loader from 'components/Loader';
import DateFilterDialog from 'components/DateFilterDialog';
import CircularLoader from 'components/CircularLoader';
import AiImage from '../../../assets/images/aiImage.png';
-import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
+import { useQuery, useMutation } from '@tanstack/react-query';
import { useNavigate, useLocation } from 'react-router-dom';
-import { fetchPercentageData, createAutomationDeliveries, getallriders, fetchorderscount } from '../../api/api';
+import { fetchPercentageData, createAutomationDeliveries, getallriders } from '../../api/api';
import { getBookings, cancelBooking, getAdminCustomers, getBookingTrack } from 'pages/api/doormileApi';
// ============================================================================
@@ -105,9 +110,17 @@ const ROW_STATUS_META = {
};
// Top-level pill tabs. GET /admin/bookings has no documented status filter
-// param, so this tab doesn't narrow the server query โ it's cosmetic until
-// the API supports it.
-const ORDERS_STATUS_TABS = [{ idx: 0, status: 'created', label: 'Bookings', color: BRAND, icon: MdLocalShipping, countKey: 'created' }];
+// param, so bookings are fetched in bulk (up to the API's pagesize cap) and
+// filtered/counted client-side by row.status โ see the bulk fetch + useMemo
+// below. `status` values here are the raw, lowercased GET /admin/bookings
+// enum (matches ROW_STATUS_META's keys), not api.js's separate generic
+// pending/accepted/delivered mapping used by the Deliveries page.
+const ORDERS_STATUS_TABS = [
+ { idx: 0, status: 'pending_pickup', label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
+ { idx: 1, status: 'converted_to_consignment', label: 'Assigned', color: '#6366f1', icon: MdCheckCircle },
+ { idx: 2, status: 'delivered', label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
+ { idx: 3, status: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdCancel }
+];
// Haversine straight-line distance in km โ GET /admin/bookings has no `kms`
// field, but does carry both pickup*/delivery* lat/lng, so this is computed
@@ -198,14 +211,21 @@ const Orders = () => {
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [tabvalue, setTabvalue] = useState(0);
- const [tabstatus, setTabstatus] = useState('Created');
- const [currentStatus, setCurrentStatus] = useState('created');
+ const [tabstatus, setTabstatus] = useState('Pending');
+ const [currentStatus, setCurrentStatus] = useState('pending_pickup');
+ // Orders selected via the checkbox column (Pending tab only) to send to
+ // the AI dispatch optimiser. Assign Orders used to submit every visible
+ // row unconditionally, including cancelled ones (no filtering existed at
+ // all) โ this makes assignment an explicit, scoped action instead.
+ const [selectedOrders, setSelectedOrders] = useState([]);
const [cancelOpen, setCancelOpen] = useState(false);
const [trackBookingId, setTrackBookingId] = useState(null);
- // GET /admin/bookings/:id/track โ response shape isn't documented beyond
- // the endpoint existing (Status: Done in jupiter2doormile.md). Rendered
- // defensively below rather than assuming specific field names.
+ // GET /admin/bookings/:id/track โ confirmed live shape (not documented in
+ // express-console-api.md): { booking: {...full booking incl. parcels[],
+ // serviceoptions[], payments[]}, assignments: [], riders: [] }. assignments/
+ // riders field names beyond that haven't been observed non-empty yet, so
+ // they're rendered defensively (see the Track Order dialog below).
const { data: trackData, isLoading: trackLoading } = useQuery({
queryKey: ['booking-track', trackBookingId],
queryFn: () => getBookingTrack(trackBookingId),
@@ -241,6 +261,7 @@ const Orders = () => {
setTabstatus(tab.label);
setCurrentStatus(tab.status);
setPage(0);
+ setSelectedOrders([]);
};
// React Queries
@@ -255,14 +276,6 @@ const Orders = () => {
refetchInterval: 15000
});
- const { data: ordersCountData, refetch: orderscountRefetch } = useQuery({
- queryKey: ['ordersCount', locationId, startdate, enddate, currentStatus, tid, locationId],
- queryFn: fetchorderscount,
- refetchOnMount: true,
- refetchOnWindowFocus: true,
- refetchInterval: 15000
- });
-
const { data: autoRiders } = useQuery({
queryKey: ['getallriders'],
queryFn: getallriders,
@@ -299,8 +312,8 @@ const Orders = () => {
autoHideDuration: 2000
});
refetchOrders();
- orderscountRefetch();
percentagedataRefetch();
+ setSelectedOrders((prev) => prev.filter((o) => o.bookingid !== orderheaderid));
setCancelOpen(false);
} else {
enqueueSnackbar(res.message || 'Failed to cancel order', {
@@ -310,7 +323,6 @@ const Orders = () => {
});
}
} catch (err) {
- console.log(err);
enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to cancel order', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
@@ -319,31 +331,62 @@ const Orders = () => {
}
};
- // Maps to /admin/bookings โ tenantid/locationid/status/date filters used by
- // the old endpoint aren't documented on this resource; pageno/pagesize are.
- const fetchOrders = async ({ pageParam = 1 }) => {
- const data = (await getBookings(pageParam, rowsPerPage)) || [];
- return {
- data,
- nextPage: data.length === rowsPerPage ? pageParam + 1 : undefined
- };
- };
-
+ // GET /admin/bookings has no status/date/search filter params documented
+ // (see express-console-api.md's "Conventions") โ fetched in one bulk call
+ // (up to the API's pagesize cap) and filtered/counted entirely client-side.
+ // Same approach api.js's getDeliveryStatusCounts already uses for the
+ // Deliveries page's chip counts, and for the same reason: cancelled orders
+ // used to show up mixed into every tab (there was no filtering at all โ
+ // the single "Bookings" tab that existed before was cosmetic), and the
+ // Assign Orders count/submit used every visible row unconditionally.
const {
- data: rowdata,
- fetchNextPage,
+ data: allBookings = [],
isLoading: isLoadingGetOrders,
- hasNextPage,
- isFetchingNextPage,
refetch: refetchOrders
- } = useInfiniteQuery({
- queryKey: [tabstatus, startdate, enddate, page, rowsPerPage, debouncedSearch, locationId],
- queryFn: fetchOrders,
- getNextPageParam: (lastPage) => lastPage.nextPage,
+ } = useQuery({
+ queryKey: ['ordersBulk'],
+ queryFn: async () => (await getBookings(1, 1000)) || [],
refetchInterval: 15000
});
- const rows = rowdata ? rowdata.pages.flatMap((p) => p.data) : [];
+ // Tab counts computed from the exact same data the table renders, so a
+ // tab's badge always matches what clicking it actually shows.
+ const statusCounts = useMemo(() => {
+ const counts = {};
+ allBookings.forEach((b) => {
+ const status = String(b.status || '').toLowerCase();
+ counts[status] = (counts[status] || 0) + 1;
+ });
+ return counts;
+ }, [allBookings]);
+
+ const filteredRows = useMemo(() => {
+ const q = debouncedSearch.trim().toLowerCase();
+ return allBookings.filter((b) => {
+ if (String(b.status || '').toLowerCase() !== currentStatus) return false;
+ if (!q) return true;
+ return (
+ b.bookingno?.toLowerCase().includes(q) ||
+ b.pickupaddress?.toLowerCase().includes(q) ||
+ b.deliveryaddress?.toLowerCase().includes(q) ||
+ b.notes?.toLowerCase().includes(q)
+ );
+ });
+ }, [allBookings, currentStatus, debouncedSearch]);
+
+ // "Load more" now reveals more of the already-fetched, already-filtered
+ // set instead of requesting another server page (the bulk fetch above
+ // already has everything) โ the IntersectionObserver/scroll-triggered
+ // reveal below is otherwise unchanged from the previous pagination UX.
+ const [visibleCount, setVisibleCount] = useState(rowsPerPage);
+ useEffect(() => {
+ setVisibleCount(rowsPerPage);
+ }, [currentStatus, debouncedSearch, rowsPerPage]);
+
+ const rows = filteredRows.slice(0, visibleCount);
+ const hasNextPage = filteredRows.length > visibleCount;
+ const isFetchingNextPage = false;
+ const fetchNextPage = useCallback(() => setVisibleCount((v) => v + rowsPerPage), [rowsPerPage]);
useEffect(() => {
if (!hasNextPage) return;
@@ -384,10 +427,12 @@ const Orders = () => {
autoHideDuration: 2000,
anchorOrigin: { vertical: 'top', horizontal: 'right' }
});
- orderscountRefetch();
refetchOrders();
setCreateloader(false);
- navigate('/nearle/dispatch/preview', {
+ // Assigned bookings' status flips away from pending_pickup server-side,
+ // so refetching above is what makes them drop out of the Pending tab.
+ setSelectedOrders([]);
+ navigate('/doormile/dispatch/preview', {
state: {
dispatchPreviewData: data,
aiMode: 1,
@@ -411,7 +456,7 @@ const Orders = () => {
});
const handleCreateDelivery = async () => {
- if (rows.length === 0) return;
+ if (selectedOrders.length === 0) return;
setIsLoading(true);
setCreateloader(true);
// The optimiser service (routes.workolik.com) itself is untouched, but the
@@ -420,7 +465,10 @@ const Orders = () => {
// object (see pickuplatitude/deliverylatitude/serviceoptions[0].estimatedprice
// instead). The solver's own expected input contract is unverified since
// it's a separate, unchanged service.
- const deliveryData = rows.map((val) => {
+ // Only the operator's checked selection is submitted โ this used to map
+ // over every visible row unconditionally, which (before status filtering
+ // existed) meant cancelled orders could get sent to the solver too.
+ const deliveryData = selectedOrders.map((val) => {
const charge = val.serviceoptions?.[0]?.estimatedprice;
return {
...val,
@@ -470,6 +518,10 @@ const Orders = () => {
}
];
+ // Table column count for skeleton rows / colSpan โ one more than usual
+ // while the checkbox column is showing (Pending tab only).
+ const colCount = currentStatus === 'pending_pickup' ? 12 : 11;
+
return (
{(fetchpercentageIsLoading || isLoadingGetOrders || isLoading || createloader) && (
@@ -479,7 +531,7 @@ const Orders = () => {
>
)}
- {rows.length > 0 && currentStatus === 'created' && (
+ {selectedOrders.length > 0 && currentStatus === 'pending_pickup' && (
{
}}
startIcon={}
>
- Assign Orders ({rows.length})
+ Assign Orders ({selectedOrders.length})
)}
@@ -606,7 +658,7 @@ const Orders = () => {
{ORDERS_STATUS_TABS.map((t) => {
const Icon = t.icon;
const active = tabvalue === t.idx;
- const count = ordersCountData?.[t.countKey] ?? 0;
+ const count = statusCounts[t.status] ?? 0;
return (
{
}
}}
>
+ {currentStatus === 'pending_pickup' && (
+
+ 0 && selectedOrders.length < rows.length}
+ checked={rows.length > 0 && selectedOrders.length === rows.length}
+ onChange={(e) => setSelectedOrders(e.target.checked ? rows : [])}
+ />
+
+ )}
#
Order
Pickup
@@ -737,7 +800,7 @@ const Orders = () => {
rows.length === 0 &&
Array.from({ length: 10 }).map((_, idx) => (
- {Array.from({ length: 11 }).map((__, ci) => (
+ {Array.from({ length: colCount }).map((__, ci) => (
@@ -747,7 +810,7 @@ const Orders = () => {
{!isLoadingGetOrders && rows.length === 0 && (
-
+
{
>
- No {currentStatus} orders
+ No {tabstatus.toLowerCase()} orders
{searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'}
@@ -793,6 +856,7 @@ const Orders = () => {
const customer = customerMap?.get(row.appcustomerid);
const kms = haversineKm(row.pickuplatitude, row.pickuplongitude, row.deliverylatitude, row.deliverylongitude);
const isCancellable = !['Cancelled', 'Converted_To_Consignment'].includes(row.status);
+ const isChecked = selectedOrders.some((o) => o.bookingid === row.bookingid);
return (
{
}
}}
>
+ {currentStatus === 'pending_pickup' && (
+
+ {
+ setSelectedOrders((prev) =>
+ e.target.checked ? [...prev, row] : prev.filter((o) => o.bookingid !== row.bookingid)
+ );
+ }}
+ onClick={(e) => e.stopPropagation()}
+ />
+
+ )}
{page * rowsPerPage + index + 1}
@@ -955,7 +1034,7 @@ const Orders = () => {
- {currentStatus === 'created' && isCancellable && (
+ {currentStatus === 'pending_pickup' && isCancellable && (
{
{rows.length !== 0 && (
-
+
{isFetchingNextPage || hasNextPage ? (
<>
@@ -1070,26 +1149,158 @@ const Orders = () => {
{/* ============================================= || Track Order || ============================================= */}
- setTrackBookingId(null)} maxWidth="xs" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}>
+ setTrackBookingId(null)} maxWidth="sm" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}>
Track Order
{trackLoading ? (
- ) : trackData ? (
-
- {Object.entries(trackData).map(([key, value]) => (
-
-
- {key}
-
-
- {typeof value === 'object' ? JSON.stringify(value) : String(value ?? 'โ')}
-
+ ) : trackData?.booking ? (
+ (() => {
+ const b = trackData.booking;
+ const assignments = trackData.assignments || [];
+ const riders = trackData.riders || [];
+ const fmt = (d) => (d ? dayjs(d).format('DD MMM, hh:mm A') : 'โ');
+ const SectionLabel = ({ children }) => (
+
+ {children}
+
+ );
+ return (
+
+
+
+
+ {b.bookingno || `#${b.bookingid}`}
+
+
+ Booked {fmt(b.createdat)}
+
+
+
+
+
+
+
+
+
+
+
+ Pickup
+
+ {b.pickupaddress || 'โ'}
+
+ {b.pickuppincode && (
+
+ Pincode {b.pickuppincode}
+
+ )}
+
+
+
+
+
+ Delivery
+
+ {b.deliveryaddress || 'โ'}
+
+ {(b.deliverypincode || b.deliverycity) && (
+
+ {[b.deliverycity, b.deliverypincode].filter(Boolean).join(' ยท ')}
+
+ )}
+
+
+ {b.notes && (
+
+
+
+ Notes
+ {b.notes}
+
+
+ )}
+
+
+ {b.parcels?.length > 0 && (
+ <>
+
+
+ Parcels
+
+ {b.parcels.map((p) => (
+ }
+ label={`${p.itemcategory || 'Item'} ยท โน${Number(p.declaredvalue || 0).toFixed(2)}`}
+ sx={{ bgcolor: tint(BRAND), border: `1px solid ${edge(BRAND)}`, fontWeight: 600 }}
+ />
+ ))}
+
+
+ >
+ )}
+
+ {b.serviceoptions?.length > 0 && (
+ <>
+
+
+ Service
+ {b.serviceoptions.map((s, i) => (
+
+
+ {s.servicetype || 'โ'}
+
+
+
+ ETA {fmt(s.estimateddeliveryat)}
+
+
+ โน{Number(s.estimatedprice || 0).toFixed(2)}
+
+
+
+ ))}
+
+ >
+ )}
+
+
+
+
+ Rider
+ {riders.length === 0 && assignments.length === 0 ? (
+
+ Not assigned to a rider yet.
+
+ ) : (
+
+ {riders.map((r, i) => (
+
+
+
+ {r.displayname || r.name || r.phone || JSON.stringify(r)}
+
+
+ ))}
+ {assignments.map((a, i) => (
+
+
+ {a.status || a.assignmentstatus || 'Assignment'}
+
+
+ {fmt(a.createdat || a.assignedat)}
+
+
+ ))}
+
+ )}
+
- ))}
-
+ );
+ })()
) : (
No tracking data available for this order yet.
diff --git a/src/pages/nearle/reports/mapWithRoute.js b/src/pages/nearle/reports/mapWithRoute.js
index 3981871..c450dee 100644
--- a/src/pages/nearle/reports/mapWithRoute.js
+++ b/src/pages/nearle/reports/mapWithRoute.js
@@ -30,8 +30,6 @@ const endIcon = new L.Icon({
});
const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => {
- console.log('additionalProps', additionalProps);
-
const mapRef = useRef(null);
const [routePoints, setRoutePoints] = useState([]);
const [loading, setLoading] = useState(false);
diff --git a/src/pages/nearle/reports/ordersDetails.js b/src/pages/nearle/reports/ordersDetails.js
index 39ae86c..cbc6288 100644
--- a/src/pages/nearle/reports/ordersDetails.js
+++ b/src/pages/nearle/reports/ordersDetails.js
@@ -1,4 +1,4 @@
-import { React, useState, useEffect, useRef } from 'react';
+import { React, useState, useEffect, useMemo, useRef } from 'react';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
// material-ui
@@ -66,7 +66,7 @@ import { FaCircleCheck } from 'react-icons/fa6';
import MapWithRoute from './mapWithRoute';
import CircularLoader from 'components/CircularLoader';
import { fetchDeliveries, fetchRidersList, gettenantlocations, getTenants } from 'pages/api/api';
-import { getDashboard, getConsignmentLogs } from 'pages/api/doormileApi';
+import { getConsignmentLogs } from 'pages/api/doormileApi';
import { CSVExport } from 'components/third-party/ReactTable';
import Loader from 'components/Loader';
import { enqueueSnackbar } from 'notistack';
@@ -117,6 +117,10 @@ const edge = (c) => a(c, '55');
const BRAND = '#C01227';
const BRAND_LIGHT = '#D25463';
+// Haversine-derived kms come back with long floating-point tails
+// (8.47316323423434532 km) โ round to 2 decimals for display everywhere.
+const formatKm = (value) => Number(value || 0).toFixed(2);
+
const SoftPaper = (props) => (
{
setTenantid(0);
@@ -353,15 +334,23 @@ export default function OrdersDetails() {
calculateTotalDistance(coData);
setMapOpen(true);
} catch (error) {
- console.log('getdeliverylogs', error);
opentoast('No Logs Found ', 'error', 2000);
} finally {
setLogsLoading(false);
}
};
- // ==============================|| fetchDeliveries (infinite) ||============================== //
-
+ // ==============================|| fetchDeliveries (bulk, auto-drained) ||============================== //
+ // GET /admin/bookings has no status/date/tenant/location/rider filter
+ // params (see express-console-api.md's "Conventions") โ fetchDeliveries in
+ // api.js only ever reads rowsPerPage off this query's key, so switching
+ // tabs/tenant/rider used to change nothing about which rows were fetched,
+ // only the cache key. The tab count badges were even further disconnected
+ // โ sourced from GET /admin/dashboard, which also takes no params, so they
+ // never matched the (also unfiltered) table either. Fixed the same way
+ // deliveries.js already was: auto-drain every page into one client-side
+ // dataset, then filter/count from that single source so the tab badge and
+ // the table can never disagree.
const {
data: deliveriesData,
isLoading: fetchDeliveriesIsLoading,
@@ -371,26 +360,54 @@ export default function OrdersDetails() {
hasNextPage,
isFetchingNextPage
} = useInfiniteQuery({
- queryKey: [
- 'fetchdeliveries',
- appId,
- userid,
- currentStatus,
- startdate,
- enddate,
- rowsPerPage,
- debouncedSearch,
- tenantid,
- locationid,
- selectedRider?.userid || 0
- ],
+ queryKey: ['fetchdeliveries-all', appId, userid, 'all', startdate, enddate, 200, '', 0, 0, 0],
queryFn: fetchDeliveries,
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
refetchOnWindowFocus: true,
refetchOnMount: true,
refetchOnReconnect: true
});
- const rows = deliveriesData?.pages.flatMap((page) => page.rows) || [];
+ const allRows = useMemo(() => (deliveriesData?.pages || []).flatMap((page) => page.rows || []), [deliveriesData]);
+
+ // Unconditional drain โ keeps pulling pages regardless of scroll position,
+ // so counts/filters are accurate even before the operator scrolls.
+ useEffect(() => {
+ if (hasNextPage && !isFetchingNextPage) fetchNextPage();
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage, allRows.length]);
+
+ const rows = useMemo(() => {
+ const wantStatus = String(currentStatus || '').toLowerCase();
+ const wantTenantId = Number(tenantid) || 0;
+ const wantRiderId = Number(selectedRider?.userid) || 0;
+ const q = String(debouncedSearch || '').trim().toLowerCase();
+ return allRows.filter((r) => {
+ if (wantStatus !== 'all' && String(r.orderstatus || '').toLowerCase() !== wantStatus) return false;
+ if (wantTenantId && Number(r.tenantid) !== wantTenantId) return false;
+ if (wantRiderId && Number(r.userid) !== wantRiderId) return false;
+ if (q) {
+ const hay = [r.deliverycustomer, r.deliveryaddress, r.pickupcustomer, r.pickupaddress, r.orderid, r.tenantname, r.ridername]
+ .map((v) => String(v || '').toLowerCase())
+ .join(' ');
+ if (!hay.includes(q)) return false;
+ }
+ return true;
+ });
+ }, [allRows, currentStatus, tenantid, selectedRider, debouncedSearch]);
+
+ // Sourced from the exact same allRows the table renders, so a tab's badge
+ // always matches what clicking it actually shows.
+ const statusCountByKey = useMemo(() => {
+ const c = { all: allRows.length, pending: 0, accepted: 0, arrived: 0, picked: 0, active: 0, delivered: 0, skipped: 0, cancelled: 0 };
+ allRows.forEach((r) => {
+ const s = String(r.orderstatus || '').toLowerCase();
+ if (c[s] !== undefined) c[s] += 1;
+ });
+ return c;
+ }, [allRows]);
+ const total = statusCountByKey.all;
+ const pendingLenght = statusCountByKey.pending;
+ const deliveredLenght = statusCountByKey.delivered;
+ const cancelLenght = statusCountByKey.cancelled;
useEffect(() => {
if (!hasNextPage) return;
@@ -456,33 +473,6 @@ export default function OrdersDetails() {
enabled: tenantid !== 0
});
- // ==============================|| status summary counts ||============================== //
- const fetchcount = async () => {
- setIsLoading(true);
- try {
- const data = (await getDashboard()) || {};
- settotal(data.total || 0);
- setPendingLenght(data.pending || 0);
- setAssignLenght(data.accepted || 0);
- setArrivedLenght(data.arrived || 0);
- setPickedLenght(data.picked || 0);
- setActiveLenght(data.active || 0);
- setDeliveredLenght(data.delivered || 0);
- setSkippedLenght(data.skipped || 0);
- setCancelLenght(data.cancelled || 0);
- } catch (err) {
- enqueueSnackbar(err.message, {
- variant: 'error',
- anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 2000
- });
- } finally {
- setIsLoading(false);
- }
- };
- useEffect(() => {
- fetchcount();
- }, [appId, startdate, enddate, currentStatus, tenantid, locationid, selectedRider]);
// CSV export payload โ flat schema preserved for backwards compatibility.
const csvData = rows?.map((order) => ({
@@ -556,7 +546,7 @@ export default function OrdersDetails() {
return (
<>
- {(isLoading ||
+ {(fetchDeliveriesIsLoading ||
fetchtenantsIsLoading ||
fetchlocationsIsLoading ||
logsLoading ||
@@ -609,7 +599,7 @@ export default function OrdersDetails() {
value={item.value ?? 0}
icon={}
color={item.color}
- loading={isLoading}
+ loading={fetchDeliveriesIsLoading}
/>
);
@@ -1198,19 +1188,19 @@ export default function OrdersDetails() {
}
- label={cancelled || row.kms == '' ? '0 km' : `${row.kms} km`}
+ label={cancelled || row.kms == '' ? '0.00 km' : `${formatKm(row.kms)} km`}
tooltip="KMS"
/>
}
- label={`${row.cumulativekms ?? 0} km`}
+ label={`${formatKm(row.cumulativekms)} km`}
tooltip="Actual KMS"
/>
}
- label={`${row.previouskms || (cancelled ? '0.00' : row.kms) || 0} km`}
+ label={`${formatKm(row.previouskms || (cancelled ? 0 : row.kms))} km`}
tooltip="Rider KMS"
/>
@@ -1508,12 +1498,17 @@ export default function OrdersDetails() {
-
+ {/* GET /admin/bookings has no per-event timestamps (accepted/
+ arrived/pickup/start/cancel) โ only assigntime (updatedat) and
+ deliverydate exist on the mapped row (see fetchDeliveries in
+ api.js), so row.pickuptime is always undefined and this cell
+ always renders the loading skeleton. The old `success` check
+ compared against a `deliverystatus` field that never existed on
+ this row either (the real field is `orderstatus`) โ dropped
+ rather than guessed, since orderstatus's confirmed values
+ (pending/accepted/active/delivered/cancelled) have no distinct
+ "picked" state to map it to. */}
+
@@ -1554,19 +1549,19 @@ export default function OrdersDetails() {
}
- label={cancelled || row.kms == '' ? '0 km' : `${row.kms} km`}
+ label={cancelled || row.kms == '' ? '0.00 km' : `${formatKm(row.kms)} km`}
tooltip="KMS"
/>
}
- label={`${row.cumulativekms ?? 0} km`}
+ label={`${formatKm(row.cumulativekms)} km`}
tooltip="Actual KMS"
/>
}
- label={`${row.previouskms || (cancelled ? '0.00' : row.kms) || 0} km`}
+ label={`${formatKm(row.previouskms || (cancelled ? 0 : row.kms))} km`}
tooltip="Rider KMS"
/>
diff --git a/src/pages/nearle/reports/profitability.js b/src/pages/nearle/reports/profitability.js
new file mode 100644
index 0000000..6a72123
--- /dev/null
+++ b/src/pages/nearle/reports/profitability.js
@@ -0,0 +1,187 @@
+import React, { useEffect, useMemo, useState } from 'react';
+import { useInfiniteQuery } from '@tanstack/react-query';
+
+// material-ui
+import { Box, Paper, TextField, Typography } from '@mui/material';
+import { MdCalendarMonth } from 'react-icons/md';
+
+import dayjs from 'dayjs';
+
+import { fetchDeliveries } from 'pages/api/api';
+import Loader from 'components/Loader';
+import PageHeader from 'components/nearle_components/PageHeader';
+import ProfitabilitySection, {
+ BASE_REVENUE,
+ BASE_KM_LIMIT,
+ EXTRA_RATE_KM,
+ VARIABLE_RATE_KM,
+ FIXED_COST_PER_SLOT,
+ BATCHES_DEFAULT
+} from '../dispatch/ProfitabilitySection';
+import { SKIPPED_STATUSES } from '../dispatch/dispatchShared';
+
+// ============================================================================
+// Standalone Profitability report โ reference project (nearle_console_express)
+// has this as its own page (reports/profitability.js); here it only existed
+// embedded inside Dispatch.js's live map. Rather than rebuild the UI (the
+// reference re-implements the profit math inline in raw MUI), this reuses
+// the already-built, already-tested ProfitabilitySection.js component as-is
+// and just supplies it with independently-fetched data โ mirrors how the
+// reference page ALSO re-fetches and re-groups independently rather than
+// reusing its own Dispatch page's pipeline (which is entangled with
+// Leaflet/GPS/zone-bucketing state not needed for a report).
+//
+// GET /admin/bookings has no server-side date/zone/status filter params (see
+// src/pages/api/CLAUDE.md) โ bulk-fetched and grouped entirely client-side,
+// same pattern as reports/ordersDetails.js and deliveries.js.
+// ============================================================================
+
+const DT = {
+ radiusCard: 14,
+ shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)',
+ textSecondary: '#64748b',
+ borderSubtle: '#e2e8f0',
+ surface: '#ffffff'
+};
+const BRAND = '#C01227';
+
+// Profit formulas + batch windows imported from ProfitabilitySection.js
+// (the single source of truth) rather than duplicated here โ used to total
+// across all riders for the daily KPI chip; per-rider numbers are computed
+// by ProfitabilitySection itself from the same `riders` data, so this must
+// match exactly or the chip and the table below it would disagree.
+const orderRevenue = (km) => (km <= BASE_KM_LIMIT ? BASE_REVENUE : BASE_REVENUE + (km - BASE_KM_LIMIT) * EXTRA_RATE_KM);
+
+const getOrderBatch = (o) => {
+ const t = o.assigntime || o.deliverydate;
+ if (!t || /^\d{4}-\d{2}-\d{2}$/.test(String(t).trim())) return null;
+ const d = dayjs(t);
+ if (!d.isValid()) return null;
+ const hour = d.hour() + d.minute() / 60;
+ const batch = BATCHES_DEFAULT.find((b) => hour >= b.startHour && hour < b.endHour);
+ return batch?.id || null;
+};
+
+const Profitability = () => {
+ const [selectedDate, setSelectedDate] = useState(dayjs().format('YYYY-MM-DD'));
+
+ const {
+ data: deliveriesData,
+ isLoading,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage
+ } = useInfiniteQuery({
+ queryKey: ['fetchdeliveries-profitability', 0, 0, 'all', selectedDate, selectedDate, 500, '', 0, 0, 0],
+ queryFn: fetchDeliveries,
+ getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined
+ });
+
+ const allRows = useMemo(() => (deliveriesData?.pages || []).flatMap((page) => page.rows || []), [deliveriesData]);
+
+ useEffect(() => {
+ if (hasNextPage && !isFetchingNextPage) fetchNextPage();
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage, allRows.length]);
+
+ // Group into { id, riderName, orders } โ flat, no zone/suburb bucketing
+ // (that's a Dispatch-map-specific concern, not needed for a report).
+ // riderkms is what ProfitabilitySection's revenue/cost math actually reads
+ // (see orderRevenue/calcRiderMetrics there) but GET /admin/bookings has no
+ // road-distance field โ mapped from the same haversine straight-line `kms`
+ // every other page in this app already uses as the distance proxy.
+ const riders = useMemo(() => {
+ const byRider = new Map();
+ allRows.forEach((row) => {
+ if (!row.userid) return;
+ const key = String(row.userid);
+ if (!byRider.has(key)) {
+ byRider.set(key, { id: key, riderName: row.ridername || `Rider #${key}`, orders: [] });
+ }
+ byRider.get(key).orders.push({ ...row, riderkms: row.kms });
+ });
+ return Array.from(byRider.values());
+ }, [allRows]);
+
+ const totalDailyProfit = useMemo(() => {
+ let profit = 0;
+ riders.forEach((rider) => {
+ let revenue = 0;
+ let kms = 0;
+ const batchesWorked = new Set();
+ rider.orders.forEach((o) => {
+ const status = String(o.orderstatus || '').toLowerCase();
+ if (SKIPPED_STATUSES.has(status)) return;
+ const dateStr = o.assigntime
+ ? dayjs(o.assigntime).format('YYYY-MM-DD')
+ : o.deliverydate
+ ? dayjs(o.deliverydate).format('YYYY-MM-DD')
+ : null;
+ if (dateStr !== selectedDate) return;
+ const km = parseFloat(o.riderkms || 0);
+ revenue += orderRevenue(km);
+ kms += km;
+ const batch = getOrderBatch(o);
+ if (batch) batchesWorked.add(batch);
+ });
+ const variableCost = kms * VARIABLE_RATE_KM;
+ const fixedCost = Math.min(batchesWorked.size, 3) * FIXED_COST_PER_SLOT;
+ profit += revenue - variableCost - fixedCost;
+ });
+ return profit;
+ }, [riders, selectedDate]);
+
+ return (
+ <>
+ {isLoading && }
+
+
+
+ e.target.value && setSelectedDate(e.target.value)}
+ InputProps={{ disableUnderline: true }}
+ sx={{ '& input': { fontWeight: 700, color: DT.textSecondary, p: 0 } }}
+ />
+
+ }
+ />
+
+
+ {!isLoading && riders.length === 0 ? (
+
+ No deliveries for {dayjs(selectedDate).format('DD MMM YYYY')}
+
+ ) : (
+
+ )}
+
+ >
+ );
+};
+
+export default Profitability;
diff --git a/src/pages/nearle/reports/ridersSummary.js b/src/pages/nearle/reports/ridersSummary.js
index 9146ef1..9d36667 100644
--- a/src/pages/nearle/reports/ridersSummary.js
+++ b/src/pages/nearle/reports/ridersSummary.js
@@ -214,15 +214,17 @@ export default function RidersSummary() {
});
// Client-side filter across rider name + id.
+ // GET /admin/milers/summary confirmed live: { milerprofileid, userid,
+ // displayname, phone, availabilitystatus, rating, assigned, accepted,
+ // rejected, completed, cancelled, delivered, riderkms, ridercharges, ... }
+ // โ no firstname/lastname/charges/deliveryamt/totalorders/pending/kms/
+ // cumulativekms field exists at all, which is why this whole page showed
+ // "โ" everywhere.
const filteredRows = useMemo(() => {
if (!rows) return [];
if (!debouncedSearch) return rows;
const q = debouncedSearch.toLowerCase().trim();
- return rows.filter((r) =>
- [`${r.firstname || ''} ${r.lastname || ''}`, String(r.userid)]
- .filter(Boolean)
- .some((field) => String(field).toLowerCase().includes(q))
- );
+ return rows.filter((r) => [r.displayname, String(r.userid)].filter(Boolean).some((field) => String(field).toLowerCase().includes(q)));
}, [rows, debouncedSearch]);
// Aggregate KPIs from the loaded summary.
@@ -231,9 +233,9 @@ export default function RidersSummary() {
return rows.reduce(
(acc, r) => {
acc.riders += 1;
- acc.orders += Number(r.totalorders) || 0;
+ acc.orders += Number(r.assigned) || 0;
acc.delivered += Number(r.delivered) || 0;
- acc.amount += Math.max(Number(r.charges) || 0, Number(r.deliveryamt) || 0);
+ acc.amount += Number(r.ridercharges) || 0;
return acc;
},
{ riders: 0, orders: 0, delivered: 0, amount: 0 }
@@ -261,7 +263,7 @@ export default function RidersSummary() {
// Total Amount sum (preserved from legacy bottom bar).
const total = useMemo(() => {
if (!rows) return 0;
- return rows.reduce((sum, row) => sum + (Number(row.deliveryamt) || 0), 0);
+ return rows.reduce((sum, row) => sum + (Number(row.ridercharges) || 0), 0);
}, [rows]);
const KPI_META = [
@@ -435,8 +437,8 @@ export default function RidersSummary() {
) : (
filteredRows.map((row, index) => {
const isOpen = openRow === row.userid;
- const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0);
- const riderName = `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || 'โ';
+ const amount = Number(row.ridercharges) || 0;
+ const riderName = row?.displayname || 'โ';
return (
{
setSelectedRider({
userid: row?.userid,
- name: `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || `Rider ${row?.userid}`
+ name: row?.displayname || `Rider ${row?.userid}`
});
setLogDetails(null);
setMapOpen(true);
@@ -504,13 +506,13 @@ export default function RidersSummary() {
>
- } />
+ } />
} />
-
- } />
+
+ } />
} />
@@ -519,18 +521,10 @@ export default function RidersSummary() {
}
- label={`${Number(row.kms || 0).toFixed(2)} km`}
+ label={`${Number(row.riderkms || 0).toFixed(2)} km`}
tooltip="KMS"
/>
-
- }
- label={`${Number(row.cumulativekms || 0).toFixed(2)} km`}
- tooltip="Actual KMS"
- />
-
#
Rider
Orders
- Pending
+ Accepted
Cancelled
Delivered
KMS
@@ -717,7 +711,7 @@ export default function RidersSummary() {
) : (
filteredRows.map((row, index) => {
const isOpen = openRow === row.userid;
- const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0);
+ const amount = Number(row.ridercharges) || 0;
return (
- {`${row?.firstname || ''} ${row?.lastname || ''}`.trim() || 'โ'}
+ {row?.displayname || 'โ'}
ID #{row.userid}
@@ -755,10 +749,10 @@ export default function RidersSummary() {
- } />
+ } />
- } />
+ } />
} />
@@ -768,20 +762,12 @@ export default function RidersSummary() {
-
- }
- label={`${Number(row.kms || 0).toFixed(2)} km`}
- tooltip="KMS"
- />
- }
- label={`${Number(row.cumulativekms || 0).toFixed(2)} km`}
- tooltip="Actual KMS"
- />
-
+ }
+ label={`${Number(row.riderkms || 0).toFixed(2)} km`}
+ tooltip="KMS"
+ />
@@ -802,7 +788,7 @@ export default function RidersSummary() {
onClick={() => {
setSelectedRider({
userid: row?.userid,
- name: `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || `Rider ${row?.userid}`
+ name: row?.displayname || `Rider ${row?.userid}`
});
setLogDetails(null);
setMapOpen(true);
@@ -881,7 +867,7 @@ export default function RidersSummary() {
variant="caption"
sx={{ fontWeight: 800, color: DT.textPrimary, letterSpacing: 0.6, textTransform: 'uppercase' }}
>
- Tenant Breakdown ยท {`${row?.firstname || ''} ${row?.lastname || ''}`.trim()}
+ Tenant Breakdown ยท {row?.displayname || ''}
diff --git a/src/pages/nearle/requests/requests.js b/src/pages/nearle/requests/requests.js
deleted file mode 100644
index 358ea51..0000000
--- a/src/pages/nearle/requests/requests.js
+++ /dev/null
@@ -1,2032 +0,0 @@
-import {
- Grid,
- Stack,
- Typography,
- Box,
- Tabs,
- Tab,
- Chip,
- Table,
- TableCell,
- TableBody,
- TableHead,
- Avatar,
- TableRow,
- IconButton,
- Collapse,
- Divider,
- List,
- ListItem,
- ListItemIcon,
- TextField,
- Button,
- InputAdornment,
- FormControl,
- OutlinedInput,
- Dialog,
- DialogContent,
- Skeleton,
- CircularProgress,
- DialogTitle,
- FormLabel,
- DialogActions,
- useMediaQuery,
- useTheme,
- Paper
-} from '@mui/material';
-import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
-import { MdPayments } from 'react-icons/md';
-import { DT, tint } from 'themes/dt/tokens';
-
-import { Autocomplete as Autocomplete1 } from '@mui/material';
-import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
-import { SearchOutlined } from '@ant-design/icons';
-import { PopupTransition } from 'components/@extended/Transitions';
-import { enqueueSnackbar } from 'notistack';
-import dayjs from 'dayjs';
-import { PhoneOutlined, MailOutlined } from '@ant-design/icons';
-
-import MainCard from 'components/MainCard';
-
-
-import { useState, useEffect } from 'react';
-import axios from 'axios';
-import Loader from 'components/Loader';
-import Transitions from 'components/@extended/Transitions';
-import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
-
-import * as React from 'react';
-
-import PropTypes from 'prop-types';
-import TableContainer from '@mui/material/TableContainer';
-import TablePagination from '@mui/material/TablePagination';
-import TableSortLabel from '@mui/material/TableSortLabel';
-import { visuallyHidden } from '@mui/utils';
-
-const Requests = () => {
- // let dispatch = useDispatch();
-
- function descendingComparator(a, b, orderBy) {
- if (b[orderBy] < a[orderBy]) {
- return -1;
- }
- if (b[orderBy] > a[orderBy]) {
- return 1;
- }
- return 0;
- }
-
- function getComparator(order, orderBy) {
- return order === 'desc' ? (a, b) => descendingComparator(a, b, orderBy) : (a, b) => -descendingComparator(a, b, orderBy);
- }
-
- function stableSort(array, comparator) {
- const stabilizedThis = array.map((el, index) => [el, index]);
- stabilizedThis.sort((a, b) => {
- const order = comparator(a[0], b[0]);
- if (order !== 0) {
- return order;
- }
- return a[1] - b[1];
- });
- return stabilizedThis.map((el) => el[0]);
- }
-
- const headCells = [
- {
- id: 'sno',
- disablePadding: true,
- label: '#'
- },
- {
- id: 'tenantname',
- numeric: false,
- disablePadding: false,
- label: 'REQUESTOR'
- },
- {
- id: 'contact',
- numeric: false,
- disablePadding: false,
- label: 'BANK'
- },
- {
- id: 'address3',
- disablePadding: false,
- label: 'IFSC'
- },
- {
- id: 'address',
- disablePadding: false,
- label: 'REF NO'
- },
- {
- id: 'amount',
- disablePadding: false,
- label: 'AMOUNT'
- },
- {
- id: 'city',
- disablePadding: false,
- label: 'REASON'
- }
- // {
- // id: 'action',
- // disablePadding: false,
- // label: 'ACTION',
- // }
- ];
-
- function EnhancedTableHead(props) {
- const { order, orderBy, onRequestSort } = props;
- const createSortHandler = (property) => (event) => {
- onRequestSort(event, property);
- };
-
- return (
-
-
- {headCells.map((headCell) => (
-
-
- {headCell.label}
- {orderBy === headCell.id ? (
-
- {order === 'desc' ? 'sorted descending' : 'sorted ascending'}
-
- ) : null}
-
-
- ))}
-
-
- );
- }
-
- EnhancedTableHead.propTypes = {
- numSelected: PropTypes.number.isRequired,
- onRequestSort: PropTypes.func.isRequired,
- onSelectAllClick: PropTypes.func.isRequired,
- order: PropTypes.oneOf(['asc', 'desc']).isRequired,
- orderBy: PropTypes.string.isRequired,
- rowCount: PropTypes.number.isRequired
- };
-
- function EnhancedTable() {
- const theme = useTheme();
- const isMobile = useMediaQuery(theme.breakpoints.down('md'));
- const [order, setOrder] = React.useState('asc');
- const [orderBy, setOrderBy] = React.useState('calories');
- const [selected, setSelected] = React.useState([]);
- const [page, setPage] = React.useState(0);
- const [rowsPerPage, setRowsPerPage] = React.useState(10);
-
- const [clientname, setClientname] = useState('');
- const [emailaddress, setEmailaddress] = useState('');
- const [mobilenumber, setMobilenumber] = useState('');
- const [regno, setRegno] = useState('');
- const [address, setAddress] = useState('');
- const [city, setCity] = useState('');
- const [zipcode, setZipcode] = useState('');
- const [contactname, setContactname] = useState('');
- const [state1, setState1] = useState('');
- const [suburb, setSuburb] = useState('');
- const [currenttenantid] = useState('');
- const [latlong, setLatlong] = useState({});
- const [alertmessage, setAlertmessage] = useState('');
- // const [toast, setToast] = useState(false);
- const [rolesarr, setRolesarr] = useState([]);
- const [roleslist] = useState([]);
- const [rolestab, setRolestab] = useState(0);
-
- const [approveid] = useState(false);
- const [disableid] = useState(false);
- const [loading1] = useState(false);
-
- const [refno, setRefno] = useState('');
- const [requestor, setRequestor] = useState('');
- const [bankname, setBankname] = useState('');
- const [amount, setAmount] = useState('');
- const [accountno, setAccountno] = useState('');
- const [ifsc, setIfsc] = useState('');
- const [reason, setReason] = useState('');
- const [expandopen, setExpandopen] = useState('');
- const [editexpandopen, setEditexpandopen] = useState('');
-
- useEffect(() => {
- setRolesarr([
- {
- sno: 1,
- role: '',
- cost: '',
- serviceid: 0,
- tenantid: 0,
- categoryid: 0,
- subcategoryid: 0,
- servicecode: '',
- servicename: '',
- unitid: 0,
- unitname: '',
- serviceamount: '',
- discountid: 0,
- taxpercent: 0,
- taxamount: 0,
- servicevalue: 0,
- categoryname: ''
- }
- ]);
- console.log(rolesarr);
- // fetchroleslist();
- }, []);
-
- // useEffect(() => {
-
- // if (alertmessage && toast) {
- // dispatch(
- // openSnackbar({
- // open: true,
- // message: alertmessage,
- // variant: 'alert',
- // anchorOrigin: { vertical: 'top', horizontal: 'right' },
- // alert: {
- // color: 'error',
-
- // }
- // })
- // )
- // }
- // }, [toast])
- const opentoast = (message) => {
- enqueueSnackbar(message, {
- variant: 'error',
- anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 2000
- });
- console.log(alertmessage);
- };
-
- // const opentoast = () => {
- // setToast(true)
-
- // setTimeout(() => {
- // setToast(false)
- // }, 2000);
-
- // }
-
- const handleAddressPlaceSelected = (place) => {
- setAddress(place.formatted_address);
- setLatlong({ lat: place.geometry.location.lat(), lng: place.geometry.location.lng() });
- let city1, state, zipcode1, suburb1;
- place.address_components.forEach((component) => {
- component.types.forEach((type) => {
- switch (type) {
- case 'locality':
- city1 = component.long_name;
- break;
- case 'administrative_area_level_1':
- state = component.long_name;
- break;
- case 'postal_code':
- zipcode1 = component.long_name;
- break;
- case 'sublocality':
- case 'sublocality_level_1':
- suburb1 = component.long_name;
- break;
- }
- });
- });
- setCity(city1 || '');
- setState1(state || '');
- setZipcode(zipcode1 || '');
- setSuburb(suburb1 || '');
- };
-
- useEffect(() => {
- console.log('rolesarr');
- console.log(rolesarr);
- }, [rolesarr]);
-
-
-
- const addarr = () => {
- let arr = rolesarr;
- if (arr[arr.length - 1].role && arr[arr.length - 1].cost) {
- arr.push({
- sno: arr.length + 1,
- cost: '',
- role: '',
- serviceid: 0,
- tenantid: 0,
- categoryid: 0,
- subcategoryid: 0,
- servicecode: '',
- servicename: '',
- unitid: 0,
- unitname: '',
- serviceamount: '',
- discountid: 0,
- taxpercent: 0,
- taxamount: 0,
- servicevalue: 0,
- categoryname: ''
- });
- setRolesarr([...arr]);
- } else {
- // setAlertmessage('Fill all Previous Details');
- opentoast('Fill all Previous Details');
- }
- };
- const deletearr = async (sno, val1) => {
- console.log(val1);
- let arr = rolesarr;
-
- if (val1.serviceid !== 0 && rolesarr.length > 1) {
- console.log([
- {
- serviceid: val1.serviceid,
- tenantid: val1.tenantid,
- categoryid: val1.categoryid,
- subcategoryid: val1.subcategoryid,
- servicecode: val1.servicecode,
- servicename: val1.servicename,
- unitid: val1.unitid,
- unitname: val1.unitname,
- serviceamount: val1.servicevalue
- }
- ]);
- try {
- await axios
- .delete(`${process.env.REACT_APP_URL2}/tenants/delete/services`, {
- data: [
- {
- serviceid: val1.serviceid,
- tenantid: val1.tenantid,
- categoryid: val1.categoryid,
- subcategoryid: val1.subcategoryid,
- servicecode: val1.servicecode,
- servicename: val1.servicename,
- unitid: val1.unitid,
- unitname: val1.unitname,
- serviceamount: val1.servicevalue
- }
- ]
- })
- .then((res) => {
- console.log('res');
- console.log(res);
- if (res.data.message === 'Deleted successful') {
- enqueueSnackbar('Client pricing Deleted', {
- variant: 'success',
- anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 2000
- });
- arr.splice(sno - 1, 1);
- arr.map((val, i) => {
- val.sno = i + 1;
- });
- console.log(arr);
- setRolesarr([...arr]);
- }
- })
- .catch((err) => {
- console.log(err);
- setLoading(false);
- });
- } catch (err) {
- console.log(err);
- setLoading(false);
- }
- } else if (rolesarr.length > 1) {
- arr.splice(sno - 1, 1);
- arr.map((val, i) => {
- val.sno = i + 1;
- });
- console.log(arr);
- setRolesarr([...arr]);
- } else if (rolesarr.length === 1) {
- setRolesarr([
- {
- sno: 1,
- role: '',
- cost: '',
- serviceid: val1.serviceid,
- tenantid: 0,
- categoryid: 0,
- subcategoryid: 0,
- servicecode: '',
- servicename: '',
- unitid: 0,
- unitname: '',
- serviceamount: '',
- discountid: 0,
- taxpercent: 0,
- taxamount: 0,
- servicevalue: 0,
- categoryname: ''
- }
- ]);
- }
- };
-
-
-
-
-
- const clientupdate = async () => {
- if (!clientname) {
- // setAlertmessage('Fill Business name');
- opentoast('Fill Business name');
- } else if (!regno) {
- // setAlertmessage('Fill Business No.');
- opentoast('Fill Business No.');
- } else if (!emailaddress) {
- // setAlertmessage('Fill Email address');
- opentoast('Fill Email address');
- } else if (!mobilenumber) {
- // setAlertmessage('Fill Mobile number');
- opentoast('Fill Mobile number');
- } else if (!contactname) {
- // setAlertmessage('Fill Contact name');
- opentoast('Fill Contact name');
- } else if (!address) {
- // setAlertmessage('Fill Address');
- opentoast('Fill Address');
- } else if (!city) {
- // setAlertmessage('Fill City name');
- opentoast('Fill City name');
- } else if (!zipcode) {
- // setAlertmessage('Fill Zip code');
- opentoast('Fill Zip code');
- } else if (!latlong.lat || !latlong.lng) {
- setAlertmessage('Fill correct address');
- opentoast('Fill correct address');
- } else if ((approveid || disableid) && !(rolesarr[0].role && rolesarr[0].cost)) {
- opentoast('Fill client pricing');
- } else {
- let obj = {
- tenantid: currenttenantid,
- registrationno: regno,
- tenantname: clientname,
- primaryemail: emailaddress,
- primarycontact: contactname,
- contactno: mobilenumber,
- address: address,
- suburb: suburb,
- city: city,
- state: state1,
- postcode: zipcode,
- latitude: latlong.lat.toString(),
- longitude: latlong.lng.toString(),
- approved: (approveid && tabvalue === 1) || (!approveid && tabvalue === 0 && !disableid) ? 1 : 0
- };
- console.log(obj);
-
- try {
- setLoading(true);
- await axios
- .put(`${process.env.REACT_APP_URL2}/tenants/update`, obj)
- .then((res) => {
- console.log('res:', res);
- if (res.data.message === 'Update successful') {
- // dispatch(
- // openSnackbar({
- // open: true,
- // message: 'Client Detail Updated Successfully',
- // variant: 'alert',
- // anchorOrigin: { vertical: 'top', horizontal: 'right' },
- // alert: {
- // color: 'success'
- // }
- // })
- // )
- enqueueSnackbar('Client Details Updated Successfully', {
- variant: 'success',
- anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 3000
- });
- setLoading(true);
-
- setTimeout(() => {
- clientdetailspending();
- clientdetailsapproved();
- setTabvalue(0);
- setLoading(false);
- }, 2000);
- }
- setLoading(false);
- })
- .catch((err) => {
- console.log(err);
- setLoading(false);
- });
- } catch (err) {
- console.log(err);
- setLoading(false);
- }
- }
- };
-
- const rolepricesubmit = async () => {
- console.log('submit');
- let arr = [];
- let objcheck = false;
- rolesarr.map((val) => {
- if (!val.role || !val.cost) {
- objcheck = true;
- }
- arr.push({
- serviceid: val.serviceid,
- tenantid: currenttenantid,
- categoryid: val.categoryid,
- subcategoryid: val.subcategoryid,
- servicecode: val.servicecode,
- servicename: val.servicename,
- unitid: val.unitid,
- unitname: val.unitname,
- serviceamount: parseFloat(val.serviceamount),
- categoryname: val.categoryname,
- subcategoryname: val.servicename
- // discountid: val.discountid,
- // taxpercent: val.taxpercent,
- // taxamount: val.taxamount,
- // servicevalue: parseFloat(val.servicevalue),
- });
- });
- console.log(arr);
- if (!objcheck) {
- try {
- setLoading(true);
- // await axios.post(`${process.env.REACT_APP_URL2}/tenants/createservice`, arr)tenants/update/services
- await axios
- .put(`${process.env.REACT_APP_URL2}/tenants/update/services`, arr)
- .then((res) => {
- console.log('res:', res);
- if (res.data.message === 'Update successful') {
- enqueueSnackbar('Service Updated Successfully', {
- variant: 'success',
- anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 2000
- });
- }
- setLoading(false);
- })
- .catch((err) => {
- console.log(err);
- setLoading(false);
- });
- } catch (err) {
- console.log(err);
- setLoading(false);
- }
- } else {
- enqueueSnackbar('Fill all Details', {
- variant: 'error',
- anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 2000
- });
- }
- };
-
- const createrequest = () => {
- if (!refno) {
- // setAlertmessage('Fill Business name');
- opentoast('Fill Reference No');
- } else if (!requestor) {
- // setAlertmessage('Fill Business No.');
- opentoast('Fill Requestor');
- } else if (!bankname) {
- // setAlertmessage('Fill Email address');
- opentoast('Fill Bank Name');
- } else if (!amount) {
- // setAlertmessage('Fill Mobile number');
- opentoast('Fill Amount');
- } else if (!accountno) {
- // setAlertmessage('Fill Contact name');
- opentoast('Fill Account No');
- } else if (!ifsc) {
- // setAlertmessage('Fill Address');
- opentoast('Fill IFSC');
- } else if (!reason) {
- // setAlertmessage('Fill City name');
- opentoast('Fill Reason');
- } else {
- let obj = {
- requestid: 0,
- requestdate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
- referenceno: refno,
- Apptypeid: 22,
- requesttype: 'staffexpenses',
- reason: reason,
- requestor: requestor,
- amount: amount,
- Accountno: accountno,
- bankname: bankname,
- ifsccode: ifsc
- };
-
- console.log(obj);
- }
- };
-
- // const rolepriceupdate = async () => {
- // console.log('submit')
- // let arr = [];
- // let objcheck = false;
- // rolesarr.map((val) => {
- // if (!val.role || !val.cost) {
- // objcheck = true;
- // }
- // arr.push({
- // serviceid: 0,
- // tenantid: currenttenantid,
- // categoryid: val.categoryid,
- // subcategoryid: val.subcategoryid,
- // servicecode: val.servicecode,
- // servicename: val.servicename,
- // unitid: val.unitid,
- // unitname: val.unitname,
- // serviceamount: parseFloat(val.serviceamount),
- // discountid: val.discountid,
- // taxpercent: val.taxpercent,
- // taxamount: val.taxamount,
- // servicevalue: parseFloat(val.servicevalue),
-
- // })
- // })
- // console.log(arr)
- // if (!objcheck) {
- // try {
- // setLoading(true)
- // // await axios.post(`${process.env.REACT_APP_URL2}/clients/createservice`, arr)
- // await axios.post(`${process.env.REACT_APP_URL2}/tenants/createservice`, arr)
-
- // // await axios.post(`${process.env.REACT_APP_URL2}/clients/createservice`, arr)
- // .then((res) => {
- // console.log('res:', res);
- // if (res.data.message === "Successful") {
-
- // enqueueSnackbar('Service created Successfully', { variant: 'success',anchorOrigin: { vertical: 'top', horizontal: 'right' },
- // autoHideDuration: 3000 })
- // }
- // setLoading(false)
- // }).catch((err) => {
- // console.log(err)
- // setLoading(false)
- // })
-
- // } catch (err) {
- // console.log(err);
- // setLoading(false)
- // }
- // } else {
-
- // enqueueSnackbar('Fill all Details', { variant: 'error',anchorOrigin: { vertical: 'top', horizontal: 'right' },
- // autoHideDuration: 2000 })
- // }
- // }
-
- const handleRequestSort = (event, property) => {
- const isAsc = orderBy === property && order === 'asc';
- setOrder(isAsc ? 'desc' : 'asc');
- setOrderBy(property);
- };
-
- const handleSelectAllClick = (event) => {
- if (event.target.checked) {
- const newSelected = rows.map((n) => n.name);
- setSelected(newSelected);
- return;
- }
- setSelected([]);
- };
-
- const handleClick = (event, name) => {
- const selectedIndex = selected.indexOf(name);
- let newSelected = [];
-
- if (selectedIndex === -1) {
- newSelected = newSelected.concat(selected, name);
- } else if (selectedIndex === 0) {
- newSelected = newSelected.concat(selected.slice(1));
- } else if (selectedIndex === selected.length - 1) {
- newSelected = newSelected.concat(selected.slice(0, -1));
- } else if (selectedIndex > 0) {
- newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1));
- }
-
- setSelected(newSelected);
- };
-
- const handleChangePage = (event, newPage) => {
- setPage(newPage);
- };
-
- const handleChangeRowsPerPage = (event) => {
- setRowsPerPage(parseInt(event.target.value, 10));
- setPage(0);
- };
-
- // const handleChangeDense = (event) => {
- // setDense(event.target.checked);
- // };
-
- const isSelected = (name) => selected.indexOf(name) !== -1;
-
- // Avoid a layout jump when reaching the last page with empty rows.
- const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - rows.length) : 0;
-
- const visibleRows = React.useMemo(
- () => stableSort(rows, getComparator(order, orderBy)).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage),
- [order, orderBy, page, rowsPerPage]
- );
-
- return (
- <>
-
- Create Request
-
-
-
-
- {/* */}
- {/* Client} sx={{ height: '100%' }}>
-
- */}
-
- {/* */}
- Reference No
- {
- setRefno(e.target.value);
- console.log(e);
- }}
- />
-
-
- Requestor
- setRequestor(e.target.value)}
- // label='Business No.'
- fullWidth
- sx={{ width: '100%' }}
- />
- {/* */}
-
-
-
- {/*
- */}
-
- Bank Name
-
- setBankname(e.target.value)}
- // label='Email'
- />
-
-
- Amount
-
- +1,
- // }}
- sx={{ width: '100%' }}
- onChange={(e) => setAmount(e.target.value)}
- />
- {/*
- */}
-
- {/*
-
- */}
-
- {/* */}
- Account No
-
- {
- setAccountno(e.target.value);
- console.log(e);
- }}
- />
-
-
- IFSC Code
-
- setIfsc(e.target.value)}
- // label='Business No.'
- fullWidth
- sx={{ width: '100%' }}
- />
- {/* */}
-
-
- Reason
-
- setReason(e.target.value)}
- // label='Business No.'
- fullWidth
- sx={{ width: '100%' }}
- />
- {/* */}
-
-
-
-
-
-
-
-
- {
- createrequest();
- }}
- >
- Update
-
- {
- dialogclose();
- }}
- color="error"
- >
- Close
-
-
-
-
-
-
-
- {isMobile && (
-
- {loading &&
- [0, 1, 2, 3, 4].map((item) => (
-
-
-
-
-
-
-
-
-
- } />
- } />
-
-
- ))}
-
- {!loading &&
- visibleRows.map((row, index) => {
- const isItemSelected = isSelected(row.sno);
- return (
- handleClick(event, row.sno)}
- header={
-
-
-
- {row.requestor ? String(row.requestor).charAt(0).toUpperCase() : '#'}
-
-
-
- {row.requestor || 'โ'}
-
-
- #{row.sno}
-
-
-
- {row.amount != null && (
-
- )}
-
- }
- >
-
-
-
-
-
-
-
-
- );
- })}
-
- {!loading && visibleRows.length === 0 && (
-
-
- No requests to show
- Requests will appear here once available.
-
- )}
-
- )}
-
-
-
-
-
- {loading && (
- <>
-
- {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((item) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
- >
- )}
-
-
- {visibleRows.map((row, index) => {
- const isItemSelected = isSelected(row.sno);
- const labelId = `enhanced-table-checkbox-${index}`;
- return (
- <>
- handleClick(event, row.sno)}
- role="checkbox"
- aria-checked={isItemSelected}
- tabIndex={-1}
- key={row.sno}
- // selected={isItemSelected}
- sx={{ cursor: 'pointer' }}
- >
- {/*
-
- */}
-
- {row.sno}
-
-
-
-
-
-
- {row.requestor}
- {/*
- {row.primaryemail}
- */}
-
-
-
-
- {row.accountno}
- {row.bankname}
-
- {row.ifsccode}
-
- {/*
-
- {row.address.slice(0, 15)}...
-
- */}
- {row.referenceno}
-
- {row.amount}
- {row.reason}
- {/*
-
- {row.reason}
-
- */}
-
-
-
-
-
-
-
- {loading1 ? (
- <>
-
- {/*
- Loading...
- */}
-
-
-
-
- >
- ) : (
- <>
- setRolestab(i)}>
-
-
-
- {rolestab === 0 && (
- <>
-
-
-
-
-
-
-
-
- {clientname}
- {regno}
-
-
-
- {/*
-
- */}
- {/*
-
-
- All orders
-
-
-
-
- Covered orders
-
-
-
- */}
-
-
-
-
-
-
-
-
-
- {/* */}
- {emailaddress}
- {/* */}
-
-
-
-
-
- {/* */}
-
- {/*
- {/* */}
-
- {/*
-
-
-
-
- city
-
-
- {/* */}
-
-
-
-
-
-
- {/* */}
- Contact Details} sx={{ height: '100%' }}>
-
-
-
-
-
- Contact Name
- {contactname}
-
-
-
- {/*
- Contact number
-
- {mobilenumber}
-
- */}
-
-
-
-
-
-
- Address
- {address}
-
-
-
-
-
-
- City
- {city}
-
-
-
-
- Zip Code
-
- {/* */}
- {zipcode}
-
-
-
-
-
-
-
-
- {/* */}
-
-
- >
- )}
- {rolestab === 1 && (
- <>
-
-
-
- {/* */}
- {rolesarr[0].role ? (
- <>
-
-
-
-
- #
- Category
-
- Skill
-
- Cost/Hr
-
- {/* */}
-
-
-
- {rolesarr.map((val1) => {
- return (
- <>
-
- {val1.sno}
- {val1.categoryname}
- {val1.role}
- {val1.cost}
-
- >
- );
- })}
-
-
-
- >
- ) : (
- <>
- No Data found
- >
- )}
- {/*
- }
-
- onClick={() => addarr()}
-
- sx={{ m: 2 }}
- variant="dashed" size="extraSmall"
- >
- ADD SKILL
-
- */}
- {/*
-
-
-
-
- SUBMIT
-
-
-
-
- */}
- {/* */}
-
-
-
- >
- )}
- >
- )}
-
-
-
-
- {/* edit page collapse */}
-
-
-
- {loading1 ? (
- <>
-
- {/*
- Loading...
- */}
-
-
-
-
- >
- ) : (
- <>
-
- setRolestab(i)}>
-
-
-
- {rolestab === 0 && (
- <>
-
-
-
- Client} sx={{ height: '100%' }}>
-
-
- {/* */}
- {/* */}
-
- {/* Client name */}
- setClientname(e.target.value)}
- />
-
- setRegno(e.target.value)}
- label="Business No."
- // placeholder='Registration Number'
- fullWidth
- sx={{ width: '100%' }}
- />
-
- {/* */}
-
-
-
-
-
- }
- sx={{ width: '100%' }}
- onChange={(e) => setEmailaddress(e.target.value)}
- label="Email"
- />
-
- +1
- }}
- sx={{ width: '100%' }}
- onChange={(e) => setMobilenumber(e.target.value)}
- />
- {/* */}
- {/* */}
- {/* */}
-
-
-
-
-
-
-
- {/* */}
- Contact Details} sx={{ height: '100%' }}>
- {/*
- */}
-
-
-
- {/* Contact Name */}
- setContactname(e.target.value)}
- label="Contact Name"
- />
- {/* name */}
-
-
-
- {/* */}
- {/*
- */}
- {/* */}
-
-
- {/* Address */}
- {/* {((row.sno === 1)) && */}
-
- <>
- {/* setAddress(e.target.value)}
- inputRef={materialRef}
-
-
- /> */}
- >
-
- {/* } */}
-
-
-
-
- {/* */}
- {/*
- */}
- {/* */}
-
-
- {/* City */}
- {/* City */}
- setCity(e.target.value)}
- />
- {/* {'data.address'} */}
-
-
-
-
- {/* Zip Code */}
- {/*
-
- */}
- setZipcode(e.target.value)}
- />
-
-
-
-
- {
- clientupdate();
- if (approveid) {
- rolepricesubmit();
- }
- }}
- >
- Update {approveid ? '& Approve' : ''}
- {disableid ? '& Disable' : ''}
-
-
-
-
- {/*
-
-
*/}
-
-
- {/* */}
-
-
-
- >
- )}
- {rolestab === 1 && (
- <>
-
-
-
-
-
-
-
-
- #
- Category
-
- Skill
-
- Cost/Hr
-
-
-
-
-
- {rolesarr.map((val1) => {
- return (
- <>
-
- {val1.sno}
- {/* {val1.sno} */}
- {val1.categoryname}
-
-
- -b.categoryname.localeCompare(a.categoryname)
- )}
- groupBy={(option) => option.categoryname}
- getOptionLabel={(option) => option.subcategoryname}
- isOptionEqualToValue={(option, value) =>
- option.subcategoryid === value.subcategoryid
- }
- onChange={(e, val) => {
- if (val) {
- console.log('eval', e);
- // console.log(skillsarr)
- // let res = rolesarr.find((val2) => val2.role === val.servicename);
- let res = rolesarr.find(
- (val2) => val2.subcategoryid === val.subcategoryid
- );
-
- console.log(val);
- if (!res) {
- let arr = rolesarr;
- arr[val1.sno - 1].role = val.subcategoryname;
- arr[val1.sno - 1].categoryname = val.categoryname;
-
- // arr[val1.sno - 1].Staffroleid = 0;
-
- arr[val1.sno - 1].categoryid = val.categoryid;
- arr[val1.sno - 1].subcategoryid = val.subcategoryid;
- // arr[val1.sno - 1].servicename = val.servicename;
- arr[val1.sno - 1].servicename = val.subcategoryname;
- // arr[val1.sno - 1].servicecode = val.servicecode;
-
- // arr[val1.sno - 1].unitid = val.serviceunit;
- arr[val1.sno - 1].unitid = val.unitid;
- // arr[val1.sno - 1].serviceid = val.serviceid;
-
- // arr[val1.sno - 1].unitname = val.serviceunitname;
- arr[val1.sno - 1].unitname = val.unitname;
-
- // setSkillsarr([...arr]);
- setRolesarr([...arr]);
- } else {
- // setAlertmessage('select different skill')
- opentoast('select different skill');
- }
- }
- console.log(val);
- }}
- renderInput={(params) => (
-
- )}
- // renderGroup={(params) => (
- //
-
- // {params.group}
- // {params.children}
-
- //
- // )}
- // options={roleslist}
- value={{
- categoryid: val1.categoryid,
- categoryname: '',
- cost: val1.serviceamount,
- label: val1.servicename,
- status: 0,
- subcategoryid: val1.subcategoryid,
- subcategoryname: val1.servicename,
- unitid: val1.unitid,
- unitname: val1.unitname
- }}
- // textContent={val1.servicename}
-
- disabled={loading ? true : false}
- />
-
-
-
- {
- let arr = rolesarr;
- if (e.target.value < 1000) {
- arr[val1.sno - 1].cost = e.target.value;
- arr[val1.sno - 1].servicevalue = e.target.value;
- arr[val1.sno - 1].serviceamount = e.target.value;
-
- setRolesarr([...arr]);
- }
-
- // forceUpdate()
- console.log(e.target.value);
- }}
- value={val1.cost}
- autoComplete="off"
- fullWidth
- />
-
-
-
- deletearr(val1.sno, val1)} color="error">
-
-
-
-
- >
- );
- })}
-
-
-
-
- }
- onClick={() => addarr()}
- sx={{ m: 2 }}
- variant="dashed"
- size="extraSmall"
- >
- ADD SKILL
-
-
-
-
-
- {/* */}
-
- Update
-
-
-
- {/* */}
-
-
-
-
-
- >
- )}
-
- >
- )}
-
-
-
-
- >
- );
- })}
- {emptyRows > 0 && (
-
-
-
- )}
-
-
-
-
- {/* */}
- {/* }
- label="Dense padding"
- /> */}
- {/* */}
-
- >
- );
- }
-
- const outerTheme = useTheme();
- const isMobile = useMediaQuery(outerTheme.breakpoints.down('md'));
- const [tabvalue, setTabvalue] = useState(0);
- const [rows, setRows] = useState([]);
- const [clientapproved, setClientApproved] = useState([]);
- const [clientpending, setClientPending] = useState([]);
- const [loading, setLoading] = useState(false);
- const [searchword, setSearchword] = useState('');
- const [dialogopen, setDialogopen] = useState(false);
-
- // const setinitial = (val)=>{
- // if(val){
-
- // console.log(val);
- // setClientname(val.tenantname)
- // }else{
- // setClientname('')
- // }
- // console.log(clientname)
-
- // }
-
- useEffect(() => {
- if (localStorage.getItem('partnerid')) {
- clientdetailspending(localStorage.getItem('partnerid'));
- clientdetailsapproved(localStorage.getItem('partnerid'));
- }
- }, []);
-
- useEffect(() => {
- // if (searchword) {
- // if (tabvalue === 0) {
- // let arr = clientapproved.filter((val) => {
- // return (val.tenantname.toLowerCase().includes(searchword.toLowerCase())
- // || val.primarycontact.toLowerCase().includes(searchword.toLowerCase())
- // || val.primaryemail.toLowerCase().includes(searchword.toLowerCase())
- // || val.city.toString().toLowerCase().includes(searchword.toLowerCase())
- // )
- // })
- // console.log(arr)
- // setRows([...arr])
- // }
- // if (tabvalue === 1) {
- // let arr = clientpending.filter((val) => {
- // return (val.tenantname.toLowerCase().includes(searchword.toLowerCase())
- // || val.primarycontact.toLowerCase().includes(searchword.toLowerCase())
- // || val.primaryemail.toLowerCase().includes(searchword.toLowerCase())
- // || val.city.toString().toLowerCase().includes(searchword.toLowerCase())
- // )
- // })
- // console.log(arr)
- // setRows([...arr])
- // }
- // }
- }, [searchword, tabvalue]);
-
- const handleChangetab = (e, i) => {
- setTabvalue(i);
- if (i === 1) setRows(clientapproved);
- if (i === 0) setRows(clientpending);
- };
-
- const clientdetailsapproved = async (tid) => {
- setLoading(true);
- try {
- await axios
- .get(`${process.env.REACT_APP_URL}/payments/requests/getpaymentrequest/?partnerid=${tid}&status=1`)
-
- .then((res) => {
- if (res.data.message === 'Successful') {
- let arr = [];
- res.data.details.map((val, i) => {
- arr = [...arr, { ...val, sno: i + 1 }];
- });
- // setArr(arr)
- setClientApproved(arr);
- console.log(res.data.details);
- setLoading(false);
- }
- })
- .catch((err) => {
- console.log(err);
- setLoading(false);
- });
- } catch (err) {
- console.log(err);
- setLoading(false);
- }
- };
-
- const clientdetailspending = async (tid) => {
- setLoading(true);
- try {
- await axios
- .get(`${process.env.REACT_APP_URL}/payments/requests/getpaymentrequest/?partnerid=${tid}&status=0`)
-
- .then((res) => {
- if (res.data.message === 'Success') {
- let arr = [];
- res.data.details.map((val, i) => {
- arr = [...arr, { ...val, sno: i + 1 }];
- });
- // setArr(arr)
- setClientPending(arr);
- setRows(arr);
- console.log(res.data.details);
- setLoading(false);
- }
- })
- .catch((err) => {
- console.log(err);
- setLoading(false);
- });
- } catch (err) {
- console.log(err);
- setLoading(false);
- }
- };
-
- const dialogclose = () => {
- setDialogopen(false);
- };
-
- return (
- <>
- {loading && }
-
-
-
-
-
-
-
-
-
- Payment Requests
-
- {
- setDialogopen(true);
- }}
- >
- Create Request
-
-
-
-
-
-
- {/* */}
-
-
-
- }
- iconPosition="end"
- />
- }
- icon={}
- />
-
-
-
-
-
- {/* */}
-
-
-
-
- >
- );
-};
-
-export default Requests;
diff --git a/src/pages/nearle/riders/RiderSubstitution.js b/src/pages/nearle/riders/RiderSubstitution.js
index 449631f..8481748 100644
--- a/src/pages/nearle/riders/RiderSubstitution.js
+++ b/src/pages/nearle/riders/RiderSubstitution.js
@@ -114,9 +114,27 @@ export default function RiderSubstitution({
[selectedDate]
);
+ // Buckets match the same morning/afternoon/evening hour ranges used
+ // elsewhere in the app (e.g. Dispatch's batch model). row.starttime's
+ // real format isn't confirmed against live data yet (this row source is
+ // currently a documented no-backend stub โ see riders.js), but this is
+ // still correct once real rows exist, rather than the toggle silently
+ // doing nothing regardless of what's selected.
+ const getShiftBatch = (starttime) => {
+ if (!starttime) return null;
+ const d = dayjs(starttime, ['HH:mm:ss', 'HH:mm', 'YYYY-MM-DD HH:mm:ss']);
+ if (!d.isValid()) return null;
+ const hour = d.hour() + d.minute() / 60;
+ if (hour >= 0 && hour < 8) return 'morning';
+ if (hour >= 9 && hour < 12.5) return 'afternoon';
+ if (hour >= 16 && hour < 19) return 'evening';
+ return null;
+ };
+
const filteredRows = React.useMemo(() => {
- return rows;
- }, [rows]);
+ if (selectedBatch === 'all') return rows;
+ return (rows || []).filter((row) => getShiftBatch(row.starttime) === selectedBatch);
+ }, [rows, selectedBatch]);
const hasAssignments = Object.values(substituteAssignments || {}).some((val) => val !== null && val !== undefined);
diff --git a/src/pages/nearle/riders/editRider.js b/src/pages/nearle/riders/editRider.js
index 413ae17..49a07bd 100644
--- a/src/pages/nearle/riders/editRider.js
+++ b/src/pages/nearle/riders/editRider.js
@@ -22,20 +22,12 @@ import { MdDirectionsBike } from 'react-icons/md';
import { DT, tint } from 'themes/dt/tokens';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
-import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
-import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
-// third-party
-// import { PatternFormat } from 'react-number-format';
-import { DatePicker } from '@mui/x-date-pickers/DatePicker';
-
// project import
import MainCard from 'components/MainCard';
import { getMiler, updateMiler, getAdminTenant } from 'pages/api/doormileApi';
import { fetchAppLocations as fetchZoneLocations } from 'pages/api/api';
-import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
import Loader from 'components/Loader';
import { enqueueSnackbar } from 'notistack';
-import dayjs from 'dayjs';
import CircularLoader from 'components/CircularLoader';
const EditRider = () => {
@@ -44,17 +36,8 @@ const EditRider = () => {
const location = useLocation();
const [riderdata, setRiderdata] = useState(null);
const navigate = useNavigate();
- const [address, setAddress] = useState('');
- const [suburb, setSuburb] = useState('');
- const [city, setCity] = useState('');
- const [state, setState] = useState('');
- const [partner, setPartner] = useState({});
- const [vehiclelist, setVehiclelist] = useState([]);
- const [accountlist, setAccountlist] = useState([]);
const [partnerlist, setPartnerlist] = useState([]);
-
- const [shiftlist, setShiftlist] = useState([]);
const [locaName, setLocoName] = useState();
const [loading, setLoading] = useState(false);
@@ -63,9 +46,8 @@ const EditRider = () => {
try {
const miler = await getMiler(id);
setRiderdata(miler);
- fetchridershifts(miler?.applocationid);
} catch (error) {
- console.log('fetchmanagerList', error);
+ // swallow
}
};
useEffect(() => {
@@ -75,81 +57,28 @@ const EditRider = () => {
fetchRiderData(location.state.riderdata.milerprofileid);
}, []);
- useEffect(() => {
- console.log('riderdata', riderdata);
- }, [riderdata, address]);
-
- useEffect(() => {
- setAddress(location.state.riderdata.address);
- setSuburb(location.state.riderdata.suburb);
- setCity(location.state.riderdata.city);
- setState(location.state.riderdata.state);
- }, []);
-
useEffect(() => {
if (localStorage.getItem('tenantid')) {
fetchtenantinfo(localStorage.getItem('tenantid'));
}
- fetchvehicle();
- fetchaccounttype();
}, []);
- useEffect(() => {
- if (partner.applocationid) {
- fetchridershifts(partner.applocationid);
- }
- }, [partner.applocationid]);
-
const fetchtenantinfo = async (tid) => {
setLoading(true);
try {
await getAdminTenant(tid);
} catch (err) {
- console.log(err);
+ // swallow
} finally {
setLoading(false);
}
};
- // No vehicle-type/account-type lookup endpoint in the new API โ these lists
- // stay empty (dropdowns render with no options) instead of crashing.
- const fetchvehicle = async () => setVehiclelist([]);
-
- const fetchaccounttype = async () => setAccountlist([]);
-
// ==============================|| fetchAppLocations ||============================== //
useEffect(() => {
fetchZoneLocations().then(setPartnerlist);
}, []);
- // No rider-shift lookup endpoint in the new API.
- const fetchridershifts = async () => setShiftlist([]);
-
- const handleAddressPlaceSelected = (place) => {
- setAddress(place.formatted_address);
-
- let city1, state1, suburb1;
- for (let i = 0; i < place.address_components.length; i++) {
- for (let j = 0; j < place.address_components[i].types.length; j++) {
- switch (place.address_components[i].types[j]) {
- case 'locality':
- city1 = place.address_components[i].long_name;
- break;
- case 'administrative_area_level_1':
- state1 = place.address_components[i].long_name;
- break;
- case 'sublocality':
- case 'sublocality_level_1':
- suburb1 = place.address_components[i].long_name;
- break;
- }
- }
- }
- setCity(city1 || '');
- setState(state1 || '');
- setSuburb(suburb1 || '');
- };
-
const updateRider = async () => {
setLoading(true);
// PUT /admin/milers/:id has no documented fields for bank/vehicle/shift
@@ -335,84 +264,9 @@ const EditRider = () => {
/>
- {/* ========================== ||Address || ========================== */}
-
-
- Address
-
-
-
- {/* ========================== || Location|| ========================== */}
-
-
- Location
- {
- setRiderdata({
- ...riderdata,
- suburb: e.target.value
- });
- setSuburb(e.target.value);
- }}
- value={suburb}
- autoComplete="off"
- />
-
-
- {/* ========================== || City|| ========================== */}
-
-
- City
- {
- setRiderdata({
- ...riderdata,
- city: e.target.value
- });
- setCity(e.target.value);
- }}
- value={city}
- autoComplete="off"
- />
-
-
- {/* ========================== || State|| ========================== */}
-
-
- State
- {
- setRiderdata({
- ...riderdata,
- state: e.target.value
- });
- setState(e.target.value);
- }}
- value={state}
- autoComplete="off"
- />
-
-
+ {/* Address/Suburb/City/State removed โ GET /admin/milers has
+ no such fields at all (confirmed live), so they always
+ loaded blank and updateRider() never submitted them. */}
{/* ========================== || Identification No|| ========================== */}
@@ -445,459 +299,25 @@ const EditRider = () => {
onChange={(event, value) => {
if (value) {
// This picker is fetchAppLocations() (hub-derived
- // cities), not a Partners resource โ the field used
- // to write a `partnerid` that doesn't exist on
- // these options (always undefined) alongside the
- // real applocationid. Only applocationid is a
- // confirmed miler field.
+ // cities), not a Partners resource โ applocationid
+ // is the only confirmed miler field it feeds.
setLocoName(value.locationname);
- fetchridershifts(value.applocationid);
setRiderdata({
...riderdata,
applocationid: value.applocationid
});
- } else {
- setPartner({});
}
}}
renderInput={(params) => }
/>
- {/* }
- label={locaName}
- options={partnerlist}
- getOptionLabel={(option) => `${option.locationname}`}
- onChange={(e, val) => {
- if (val) {
- setPartner(val);
- setRiderdata({
- ...riderdata,
- partnerid: val.partnerid
- });
- } else {
- setPartner({});
- }
- }}
- freeSolo
- /> */}
-
-
-
-
- {/* || =========================================== || Charges || =========================================== || */}
-
-
- Charges
-
- }
- >
-
- {/* ========================== || Shift Type || ========================== */}
-
-
-
- Shift Type
-
- (
-
- )}
- // disabled
- options={shiftlist}
- onChange={(e, val) => {
- if (val) {
- console.log('shift', val);
- setRiderdata({
- ...riderdata,
- shiftid: val.shiftid,
- additionalkm: val.additionalkm,
- additionalcharges: val.additionalcharges,
- basefare: val.basefare,
- starttime: val.starttime,
- endtime: val.endtime
- });
- }
- }}
- freeSolo
- />
-
-
- {/* ========================== || Base Fare || ========================== */}
-
-
-
- Base Fare
- {
- setRiderdata({
- ...riderdata,
- basefare: e.target.value
- });
- }}
- value={riderdata?.basefare}
- autoComplete="off"
- disabled
- />
-
-
- {/* ========================== || Additional Kms || ========================== */}
-
-
-
- Additional Kms
- {
- setRiderdata({
- ...riderdata,
- additionalkm: e.target.value
- });
- }}
- value={riderdata?.additionalkm}
- autoComplete="off"
- disabled
- />
-
-
- {/* ========================== || Other Charges || ========================== */}
-
-
-
- Other Charges
- {
- setRiderdata({
- ...riderdata,
- additionalcharges: e.target.value
- });
- }}
- value={riderdata?.additionalcharges}
- autoComplete="off"
- disabled
- />
-
-
-
-
-
- {/* || =========================================== || Bank Details || =========================================== || */}
-
-
- Bank Details
-
- }
- >
-
- {' '}
- {/* ========================== || Account No|| ========================== */}
-
-
- Account No
- {
- setRiderdata({
- ...riderdata,
- accountno: e.target.value
- });
- }}
- autoComplete="off"
- />
-
- {' '}
- {/* ========================== || Account Name || ========================== */}
-
-
- Account Name
- {
- setRiderdata({
- ...riderdata,
- accountname: e.target.value
- });
- }}
- autoComplete="off"
- />
-
- {' '}
- {/* ========================== || Account Type || ========================== */}
-
-
- Account Type
-
- (
-
- )}
- // disabled
- options={accountlist}
- // value={clientdetail}
- onChange={(e, val) => {
- console.log('ac type', val);
- if (val) {
- setRiderdata({
- ...riderdata,
- accounttype: val.label
- });
- // fetchroles(val.tenantid);
- }
- }}
- freeSolo
- />
-
- {' '}
- {/* ========================== || Bank Name|| ========================== */}
-
-
- Bank Name
- {
- setRiderdata({
- ...riderdata,
- bankname: e.target.value
- });
- }}
- autoComplete="off"
- />
-
- {' '}
- {/* ========================== || IFSC Code || ========================== */}
-
-
- IFSC Code
- {
- setRiderdata({
- ...riderdata,
- ifsccode: e.target.value
- });
- }}
- autoComplete="off"
- />
-
-
- {/* ========================== || Branch || ========================== */}
-
-
- Branch
- {
- setRiderdata({
- ...riderdata,
- branch: e.target.value
- });
- }}
- autoComplete="off"
- />
-
-
-
-
-
- {/* || =========================================== || Vehicle Details || =========================================== || */}
-
-
- Vehicle Details
-
- }
- >
-
- {/* ========================== || Vehicle Name || ========================== */}
-
-
- Vehicle Name
- (
-
- )}
- // disabled
- options={vehiclelist}
- // value={clientdetail}
- onChange={(e, val) => {
- if (val) {
- console.log('vehi', val);
- setRiderdata({
- ...riderdata,
- vehiclename: val.label,
- vehicleid: val.apptypeid
- });
-
- // fetchroles(val.tenantid);
- }
- }}
- freeSolo
- />
-
- {' '}
- {/* ========================== || Vehicle No || ========================== */}
-
-
- Vehicle No
- {
- setRiderdata({
- ...riderdata,
- vehicleno: e.target.value
- });
- }}
- value={riderdata?.vehicleno}
- autoComplete="off"
- />
-
- {' '}
- {/* ========================== || Model Year || ========================== */}
-
-
- Model Year
- {
- setRiderdata({
- ...riderdata,
- model: e.target.value
- });
- }}
- value={riderdata?.model}
- autoComplete="off"
- />
-
- {' '}
- {/* ========================== || Vehicle Color || ========================== */}
-
-
- Vehicle Color
- {
- setRiderdata({
- ...riderdata,
- color: e.target.value
- });
- }}
- value={riderdata?.color}
- autoComplete="off"
- />
-
- {' '}
- {/* ========================== || License No || ========================== */}
-
-
- License No
- {
- setRiderdata({
- ...riderdata,
- licenseno: e.target.value
- });
- }}
- value={riderdata?.licenseno}
- autoComplete="off"
- />
-
- {' '}
- {/* ========================== || Insurance No || ========================== */}
-
-
- Insurance No
- {
- setRiderdata({
- ...riderdata,
- insuranceno: e.target.value
- });
- }}
- value={riderdata?.insuranceno}
- autoComplete="off"
- />
-
- {' '}
- {/* ========================== || Insurance Expiry Date || ========================== */}
-
-
- Insurance Expiry Date
-
- {
- setRiderdata({
- ...riderdata,
- insurancedate: dayjs(e.$d).format('YYYY-MM-DD 00:00:00')
- });
- }}
- sx={{ width: '100%' }}
- // disablePast
- // minDate={dayjs().add(1, 'day')}
- />
-
+ {/* Charges/Bank Details/Vehicle Details sections removed โ PUT
+ /admin/milers/:id has no matching fields for any of them (no
+ shift/basefare/bank/vehicle sub-object on a miler), so nothing
+ typed into them ever reached the server. */}
{/* ================= FIXED BOTTOM ACTION ================= */}
diff --git a/src/pages/nearle/riders/riders.js b/src/pages/nearle/riders/riders.js
index c9205e4..a580ac6 100644
--- a/src/pages/nearle/riders/riders.js
+++ b/src/pages/nearle/riders/riders.js
@@ -68,7 +68,7 @@ import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'compon
import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar';
import CircularLoader from 'components/CircularLoader';
import { fetchAllRiders, getallridersummary } from 'pages/api/api';
-import { getMilers, blockMiler, assignMilerVehicle, getVehicles } from 'pages/api/doormileApi';
+import { getMilers, blockMiler, assignMilerVehicle, getVehicles, getMilerLogs, getMiler } from 'pages/api/doormileApi';
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import LoaderWithImage from 'components/nearle_components/LoaderWithImage';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
@@ -144,6 +144,10 @@ const STATUS_META = {
available: { label: 'Available', color: '#10b981', icon: MdCheckCircle },
assigned: { label: 'Assigned', color: '#6366f1', icon: MdTwoWheeler },
on_pickup: { label: 'On Pickup', color: '#14b8a6', icon: MdTwoWheeler },
+ // Confirmed live: PUT /admin/milers/:id/block sets availabilitystatus to
+ // "Blocked" โ a value this map didn't have, so a blocked rider fell
+ // through to "unknown" instead of clearly reading as blocked.
+ blocked: { label: 'Blocked', color: '#7f1d1d', icon: MdBlock },
idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime },
unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 }
};
@@ -178,7 +182,7 @@ const Riders = () => {
const roleid = localStorage.getItem('roleid');
const totalCols = roleid == 1 ? 11 : 10;
const [logsRow, setLogsRow] = useState(null);
- const [riderLogsdata] = useState(null);
+ const [riderLogsdata, setRiderLogsdata] = useState(null);
const [historyDate, setHistoryDate] = useState(dayjs());
const [historyStatus, setHistoryStatus] = useState('all');
const [selectedDate, setSelectedDate] = useState(dayjs());
@@ -281,10 +285,37 @@ const Riders = () => {
});
// ==============================|| getRiderLogs (riders)||============================== //
- // No live GPS/battery/periodic-log endpoint in the new API โ the expanded
- // row still opens, showing the panel's built-in "โ" placeholders.
- const getRiderLogs = async () => {
- OpenToast('Live rider logs are not available on the Doormile Express API yet.', 'warning', 2500);
+ // GET /admin/milers/:id/logs is confirmed live (Status: Done) โ id is
+ // milerprofileid, not userid. A log entry's own field names are still
+ // unconfirmed (every rider in the tenant we tested against had an empty
+ // log history), so this falls back to the miler's own last-known position
+ // (currentlatitude/currentlongitude/lastlocationupdatedat/
+ // availabilitystatus โ all confirmed fields) when there's no ping yet,
+ // rather than showing nothing.
+ const getRiderLogs = async (milerprofileid) => {
+ if (!milerprofileid) return;
+ setRiderLogsdata(null);
+ try {
+ const logs = await getMilerLogs(milerprofileid, undefined, undefined, 1);
+ const latest = Array.isArray(logs) ? logs[0] : null;
+ if (latest) {
+ setRiderLogsdata(latest);
+ } else {
+ const miler = await getMiler(milerprofileid);
+ setRiderLogsdata(
+ miler
+ ? {
+ latitude: miler.currentlatitude,
+ longitude: miler.currentlongitude,
+ logdate: miler.lastlocationupdatedat || miler.lastpingat,
+ status: miler.availabilitystatus
+ }
+ : null
+ );
+ }
+ } catch (err) {
+ OpenToast('Could not load rider logs', 'error', 2000);
+ }
};
// ==============================|| fetchAllRiders||============================== //
@@ -312,8 +343,10 @@ const Riders = () => {
// no-ops until filtered client-side here.
const filteredRows = rows.filter((row) => {
// "Active" here means currently online/working (Available/Assigned/
- // On_Pickup) โ the only real "not working" state is Offline.
- if (tabvalue === 1 && String(row.availabilitystatus || '').toLowerCase() === 'offline') return false;
+ // On_Pickup) โ Offline and Blocked (confirmed live enum) both mean not
+ // working.
+ const status = String(row.availabilitystatus || '').toLowerCase();
+ if (tabvalue === 1 && (status === 'offline' || status === 'blocked')) return false;
if (!debouncedSearch) return true;
const q = debouncedSearch.toLowerCase();
return [row.displayname, row.phone].some((v) => String(v || '').toLowerCase().includes(q));
@@ -992,7 +1025,7 @@ const Riders = () => {
filteredRows?.map((row, index) => {
const statusMeta = getRowStatusMeta(row);
const StatusIcon = statusMeta.icon;
- const expanded = logsRow === row.userid;
+ const expanded = logsRow === row.milerprofileid;
return (
{
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
}}
onClick={() => {
- if (row.userid == logsRow) {
+ if (row.milerprofileid == logsRow) {
setLogsRow(null);
} else {
- setLogsRow(row.userid);
- getRiderLogs(row.userid);
+ setLogsRow(row.milerprofileid);
+ getRiderLogs(row.milerprofileid);
}
}}
>
@@ -1343,7 +1376,7 @@ const Riders = () => {
filteredRows?.map((row, index) => {
const statusMeta = getRowStatusMeta(row);
const StatusIcon = statusMeta.icon;
- const expanded = logsRow === row.userid;
+ const expanded = logsRow === row.milerprofileid;
return (
{
'&:hover': { bgcolor: '#0ea5e9', color: '#fff' }
}}
onClick={() => {
- if (row.userid == logsRow) {
+ if (row.milerprofileid == logsRow) {
setLogsRow(null);
} else {
- setLogsRow(row.userid);
- getRiderLogs(row.userid);
+ setLogsRow(row.milerprofileid);
+ getRiderLogs(row.milerprofileid);
}
}}
>
diff --git a/src/pages/nearle/vehicles/vehicles.js b/src/pages/nearle/vehicles/vehicles.js
index 80d61ca..357e396 100644
--- a/src/pages/nearle/vehicles/vehicles.js
+++ b/src/pages/nearle/vehicles/vehicles.js
@@ -64,7 +64,12 @@ const emptyForm = {
maxvolume: '',
partner: null,
batterypercentage: '',
- status: 'active'
+ // Confirmed live: POST /admin/vehicles rejects "active"/"Active"/
+ // "inactive"/"Inactive"/"Unavailable"/"Maintenance"/"Offline" โ the only
+ // value that worked was "Available". Status is a required field with a
+ // narrow, still not fully mapped enum, so this only offers the one
+ // confirmed-working value rather than guessing a second one that fails.
+ status: 'Available'
};
const Vehicles = () => {
@@ -88,7 +93,10 @@ const Vehicles = () => {
const stats = useMemo(() => {
const total = vehicles.length;
- const active = vehicles.filter((v) => String(v.status).toLowerCase() === 'active').length;
+ // Only 'Available' is a confirmed-valid status on this resource (see the
+ // comment on emptyForm.status above) โ no vehicle can ever have a status
+ // of 'active', so counting against that string always read 0.
+ const active = vehicles.filter((v) => String(v.status).toLowerCase() === 'available').length;
const lowBattery = vehicles.filter((v) => Number(v.batterypercentage) > 0 && Number(v.batterypercentage) < 20).length;
return { total, active, lowBattery };
}, [vehicles]);
@@ -137,7 +145,7 @@ const Vehicles = () => {
maxvolume: row.maxvolume ?? '',
partner: partnerMap.get(row.partnerid) || null,
batterypercentage: row.batterypercentage ?? '',
- status: row.status || 'active'
+ status: row.status || 'Available'
});
setDialogOpen(true);
};
@@ -184,7 +192,7 @@ const Vehicles = () => {
} color={BRAND} />
- } color="#10b981" />
+ } color="#10b981" />
} color="#f59e0b" />
@@ -323,10 +331,12 @@ const Vehicles = () => {
Status
-
+ {/* Only "Available" is confirmed to work against POST
+ /admin/vehicles (Active/Inactive/Unavailable/Maintenance/
+ Offline all rejected live) โ not offering a second option
+ that's known to fail. On edit this preserves whatever the
+ vehicle's real status already is. */}
+
diff --git a/src/routes/MainRoutes.js b/src/routes/MainRoutes.js
index c4b309f..966f6d4 100644
--- a/src/routes/MainRoutes.js
+++ b/src/routes/MainRoutes.js
@@ -30,10 +30,12 @@ const Createorder1 = Loadable(lazy(() => import('pages/nearle/orders/createorder
const MultipleOrders = Loadable(lazy(() => import('pages/nearle/orders/multipleOrders')));
const Createclient = Loadable(lazy(() => import('pages/nearle/clients/createclient')));
+const CreateCustomer = Loadable(lazy(() => import('pages/nearle/clients/createCustomer')));
const OrdersSummary = Loadable(lazy(() => import('pages/nearle/reports/ordersSummary')));
const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails')));
const RidersSummary = Loadable(lazy(() => import('pages/nearle/reports/ridersSummary')));
+const Profitability = Loadable(lazy(() => import('pages/nearle/reports/profitability')));
const Riders = Loadable(lazy(() => import('pages/nearle/riders/riders')));
const Createrider = Loadable(lazy(() => import('pages/nearle/riders/createrider')));
@@ -124,6 +126,10 @@ const MainRoutes = {
{
path: 'riderssummary',
element:
+ },
+ {
+ path: 'profitability',
+ element:
}
]
},
@@ -139,6 +145,10 @@ const MainRoutes = {
path: 'customers',
element:
},
+ {
+ path: 'customer/create',
+ element:
+ },
{
path: 'hubs',
element:
diff --git a/src/store/reducers/actions.js b/src/store/reducers/actions.js
deleted file mode 100644
index d5a0eb8..0000000
--- a/src/store/reducers/actions.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// action - account reducer
-export const LOGIN = '@auth/LOGIN';
-export const LOGOUT = '@auth/LOGOUT';
-export const REGISTER = '@auth/REGISTER';
diff --git a/src/store/reducers/auth.js b/src/store/reducers/auth.js
deleted file mode 100644
index c9ebee3..0000000
--- a/src/store/reducers/auth.js
+++ /dev/null
@@ -1,48 +0,0 @@
-// action - state management
-import { REGISTER, LOGIN, LOGOUT } from './actions';
-
-// initial state
-export const initialState = {
- // isLoggedIn: false,
- // isInitialized: false,
- // user: null
- isLoggedIn: true,
- isInitialized: true,
- user: null
-};
-
-// ==============================|| AUTH REDUCER ||============================== //
-
-const auth = (state = initialState, action) => {
- switch (action.type) {
- case REGISTER: {
- const { user } = action.payload;
- return {
- ...state,
- user
- };
- }
- case LOGIN: {
- const { user } = action.payload;
- return {
- ...state,
- isLoggedIn: true,
- isInitialized: true,
- user
- };
- }
- case LOGOUT: {
- return {
- ...state,
- isInitialized: true,
- isLoggedIn: false,
- user: null
- };
- }
- default: {
- return { ...state };
- }
- }
-};
-
-export default auth;
diff --git a/src/store/reducers/loginUserSlice.js b/src/store/reducers/loginUserSlice.js
index fe97720..220a2cf 100644
--- a/src/store/reducers/loginUserSlice.js
+++ b/src/store/reducers/loginUserSlice.js
@@ -14,18 +14,12 @@ const loginUserSlice = createSlice({
state.user = action.payload;
},
- updateUserFcmToken(state, action) {
- if (state.user) {
- state.user.userfcmtoken = action.payload;
- }
- },
-
logoutUser() {
return initialState; // ๐ฅ clears entire slice
}
}
});
-export const { setLoginUser, updateUserFcmToken, logoutUser } = loginUserSlice.actions;
+export const { setLoginUser, logoutUser } = loginUserSlice.actions;
export default loginUserSlice.reducer;
diff --git a/src/store/reducers/menu.js b/src/store/reducers/menu.js
index 589c3d4..ce1af93 100644
--- a/src/store/reducers/menu.js
+++ b/src/store/reducers/menu.js
@@ -1,8 +1,5 @@
// third-party
-import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
-
-// project import
-import axios from 'utils/axios';
+import { createSlice } from '@reduxjs/toolkit';
// initial state
const initialState = {
@@ -18,11 +15,6 @@ const initialState = {
// ==============================|| SLICE - MENU ||============================== //
-export const fetchMenu = createAsyncThunk('', async () => {
- const response = await axios.get('/api/menu/dashboard');
- return response.data;
-});
-
const menu = createSlice({
name: 'menu',
initialState,
@@ -53,12 +45,6 @@ const menu = createSlice({
setSelectedMenu(state, action) {
state.selectedMenu = action.payload;
}
- },
-
- extraReducers(builder) {
- builder.addCase(fetchMenu.fulfilled, (state, action) => {
- state.menu = action.payload.dashboard;
- });
}
});
diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json
index 67a4805..7497109 100644
--- a/src/utils/locales/en.json
+++ b/src/utils/locales/en.json
@@ -14,6 +14,7 @@
"ordersummary": "Orders Summary",
"ordersdetails": "Orders Details",
"riderssummary": "Riders Summary",
+ "profitability": "Profitability",
"dispatch": "Dispatch",
"customers": "Customers",
"fleetops": "Fleet & Ops",
diff --git a/src/utils/logger.js b/src/utils/logger.js
index 549143b..1fc86cf 100644
--- a/src/utils/logger.js
+++ b/src/utils/logger.js
@@ -15,11 +15,6 @@ const style = (bg, color) => `background: ${bg}; color: ${color}; padding: 2px 5
const PREFIX = '%c[Doormile Express]';
const PREFIX_STYLE = style('#2563eb', '#ffffff');
-// Capture original console methods before any global overrides occur
-const originalLog = console.log;
-const originalWarn = console.warn || console.log;
-const originalError = console.error || console.log;
-
const print = (levelName, args, labelStyle) => {
const levelValue = LOG_LEVELS[levelName];
if (levelValue < GLOBAL_LOG_LEVEL) return;
@@ -30,10 +25,17 @@ const print = (levelName, args, labelStyle) => {
const formatPrefix = `${PREFIX}%c ${levelName}`;
const styles = [PREFIX_STYLE, labelStyle];
+ // Read console.log/warn/error fresh on every call rather than capturing
+ // them at module-import time โ index.js's production console-disable
+ // override (console.log = () => {}, etc.) runs as part of its own
+ // top-level code, which executes AFTER this module's imports are
+ // evaluated. Capturing eagerly grabbed the real methods before that
+ // override existed, so logger.warn/error silently bypassed the prod
+ // safety net and kept printing to the browser console.
const consoleMethod =
- levelName === 'ERROR' ? originalError :
- levelName === 'WARN' ? originalWarn :
- originalLog;
+ levelName === 'ERROR' ? console.error || console.log :
+ levelName === 'WARN' ? console.warn || console.log :
+ console.log;
if (isMessageString) {
consoleMethod(