Migrate console off jupiter.nearle.app to the Doormile Express API

Retires REACT_APP_URL/URL2/URL3 in favor of REACT_APP_DOORMILE_URL across
every page (orders, deliveries, riders, tenants, pricing, profile, reports,
dispatch). Fixes several field-mapping and envelope-check bugs found along
the way, most notably that /admin/milers/:id routes (block, assign-vehicle,
edit, notify) key off milerprofileid, not userid, and that a miler's real
fields are phone/availabilitystatus/displayname, not contactno/status/
firstname+lastname (confirmed against a live read-only session).

Also fixes several silent-failure bugs uncovered during that audit: order
creation and order cancellation showed a success toast but gave no feedback
at all on failure (createorder1.js had a dead notifyadmin() call that left
the loading spinner stuck forever on every failed submit), and Tenants.js's
pricing/profile updates never surfaced a failed response to the operator.

The AI dispatch optimiser (routes.workolik.com/routemate.workolik.com) and
its jupiter.nearle.app delivery-commit call remain untouched by design —
separate solver service with no equivalent in the new API.
This commit is contained in:
2026-08-06 19:26:23 +05:30
parent 0a36212ac8
commit 59f31c8adf
39 changed files with 7006 additions and 9170 deletions

View File

@@ -1,5 +1,4 @@
import { React, useState, useEffect, useRef } from 'react';
import axios from 'axios';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
// material-ui
@@ -67,6 +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 { CSVExport } from 'components/third-party/ReactTable';
import Loader from 'components/Loader';
import { enqueueSnackbar } from 'notistack';
@@ -227,205 +227,6 @@ const StampCell = ({ value, formatDate, formatTime, success }) => {
// ==============================|| Orders Details ||============================== //
// Haversine distance between two [lat, lng] points in kilometers.
function haversineKm(a, b) {
const R = 6371; // km
const toRad = (d) => (d * Math.PI) / 180;
const lat1 = toRad(a[0]);
const lat2 = toRad(b[0]);
const dLat = toRad(b[0] - a[0]);
const dLon = toRad(b[1] - a[1]);
const s = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
return 2 * R * Math.asin(Math.min(1, Math.sqrt(s)));
}
function kalmanSmoothGps(pings, options = {}) {
if (!Array.isArray(pings) || pings.length === 0) return [];
// 1. Filter out obviously invalid coordinate pings (e.g. 0,0 or NaN)
const cleanedPings = pings.filter(p =>
Number.isFinite(p.lat) &&
Number.isFinite(p.lng) &&
(Math.abs(p.lat) > 0.1 || Math.abs(p.lng) > 0.1)
);
if (cleanedPings.length === 0) return [];
if (cleanedPings.length === 1) {
return [{ lat: cleanedPings[0].lat, lng: cleanedPings[0].lng, logdate: cleanedPings[0].logdate, _ts: cleanedPings[0]._ts }];
}
const processNoise =
options.processNoise != null ? options.processNoise : 1e-10;
const measurementNoise =
options.measurementNoise != null ? options.measurementNoise : 2e-9;
const outlierGate =
options.outlierGate != null ? options.outlierGate : 9.0;
const maxSpeedKmh =
options.maxSpeedKmh != null ? options.maxSpeedKmh : 120;
const tsOf = (p) =>
p._ts || (p.logdate ? new Date(p.logdate).getTime() : 0);
// 2. Scan forward to find the first valid starting anchor
let startIdx = 0;
while (startIdx < cleanedPings.length - 1) {
const p0 = cleanedPings[startIdx];
const p1 = cleanedPings[startIdx + 1];
const ts0 = tsOf(p0);
const ts1 = tsOf(p1) || ts0 + 1000;
const dtSec = Math.max(0.001, (ts1 - ts0) / 1000);
const km = haversineKm([p0.lat, p0.lng], [p1.lat, p1.lng]);
const speedKmh = (km / dtSec) * 3600;
if (speedKmh <= maxSpeedKmh) {
break;
} else {
// Speed is too high. Check if p1->p2 is normal (meaning p0 is the outlier)
if (startIdx + 2 < cleanedPings.length) {
const p2 = cleanedPings[startIdx + 2];
const ts2 = tsOf(p2) || ts1 + 1000;
const dtSec12 = Math.max(0.001, (ts2 - ts1) / 1000);
const km12 = haversineKm([p1.lat, p1.lng], [p2.lat, p2.lng]);
const speedKmh12 = (km12 / dtSec12) * 3600;
if (speedKmh12 <= maxSpeedKmh) {
startIdx = startIdx + 1;
continue;
}
}
startIdx++;
}
}
// 3. Teleport filter starting from the valid anchor
const accepted = [cleanedPings[startIdx]];
let lastTs = tsOf(cleanedPings[startIdx]);
for (let i = startIdx + 1; i < cleanedPings.length; i++) {
const p = cleanedPings[i];
const ts = tsOf(p) || lastTs + 1000;
const dtSec = Math.max(0.001, (ts - lastTs) / 1000);
const prev = accepted[accepted.length - 1];
const km = haversineKm([prev.lat, prev.lng], [p.lat, p.lng]);
const speedKmh = (km / dtSec) * 3600;
if (speedKmh > maxSpeedKmh) continue;
accepted.push(p);
lastTs = ts;
}
if (accepted.length < 2) {
return accepted.map((p) => ({ lat: p.lat, lng: p.lng, logdate: p.logdate, _ts: p._ts }));
}
// Run a 1D Kalman + RTS smoother over one axis. Returns smoothed
// positions parallel to `accepted`.
const smoothAxis = (axisKey) => {
const N = accepted.length;
const xPost = new Array(N);
const pPost = new Array(N);
const xPrior = new Array(N);
const pPrior = new Array(N);
const dtArr = new Array(N);
const ts0 = tsOf(accepted[0]);
const ts1 = tsOf(accepted[1]);
const dt01 = Math.max(0.1, (ts1 - ts0) / 1000);
const v0 = (accepted[1][axisKey] - accepted[0][axisKey]) / dt01;
xPost[0] = [accepted[0][axisKey], v0];
pPost[0] = [measurementNoise, 0, 0, 1];
xPrior[0] = xPost[0].slice();
pPrior[0] = pPost[0].slice();
dtArr[0] = 0;
let prevTs = ts0;
for (let i = 1; i < N; i++) {
const ts = tsOf(accepted[i]) || prevTs + 1000;
const dt = Math.max(0.1, (ts - prevTs) / 1000);
prevTs = ts;
dtArr[i] = dt;
// Predict
const [xPrev, vPrev] = xPost[i - 1];
const xPredPos = xPrev + vPrev * dt;
const xPredVel = vPrev;
const [pp00, pp01, pp10, pp11] = pPost[i - 1];
const dt2 = dt * dt;
const dt3 = dt2 * dt;
const dt4 = dt3 * dt;
const np00 = pp00 + dt * (pp01 + pp10) + dt2 * pp11 + (dt4 / 4) * processNoise;
const np01 = pp01 + dt * pp11 + (dt3 / 2) * processNoise;
const np10 = pp10 + dt * pp11 + (dt3 / 2) * processNoise;
const np11 = pp11 + dt2 * processNoise;
xPrior[i] = [xPredPos, xPredVel];
pPrior[i] = [np00, np01, np10, np11];
// Update
const z = accepted[i][axisKey];
const y = z - xPredPos;
const S = np00 + measurementNoise;
const mahal2 = (y * y) / S;
if (mahal2 > outlierGate) {
xPost[i] = [xPredPos, xPredVel];
pPost[i] = [np00, np01, np10, np11];
continue;
}
const K0 = np00 / S;
const K1 = np10 / S;
const newPos = xPredPos + K0 * y;
const newVel = xPredVel + K1 * y;
xPost[i] = [newPos, newVel];
pPost[i] = [
(1 - K0) * np00,
(1 - K0) * np01,
np10 - K1 * np00,
np11 - K1 * np01
];
}
// RTS backward smoother
const xSmooth = new Array(N);
xSmooth[N - 1] = xPost[N - 1].slice();
for (let i = N - 2; i >= 0; i--) {
const dt = dtArr[i + 1];
const [pp00, pp01, pp10, pp11] = pPost[i];
const a = pp00 + dt * pp01;
const b = pp01;
const c = pp10 + dt * pp11;
const d = pp11;
const [q00, q01, q10, q11] = pPrior[i + 1];
const det = q00 * q11 - q01 * q10;
if (!Number.isFinite(det) || Math.abs(det) < 1e-30) {
xSmooth[i] = xPost[i].slice();
continue;
}
const inv00 = q11 / det;
const inv01 = -q01 / det;
const inv10 = -q10 / det;
const inv11 = q00 / det;
const c00 = a * inv00 + b * inv10;
const c01 = a * inv01 + b * inv11;
const c10 = c * inv00 + d * inv10;
const c11 = c * inv01 + d * inv11;
const dxPos = xSmooth[i + 1][0] - xPrior[i + 1][0];
const dxVel = xSmooth[i + 1][1] - xPrior[i + 1][1];
xSmooth[i] = [
xPost[i][0] + c00 * dxPos + c01 * dxVel,
xPost[i][1] + c10 * dxPos + c11 * dxVel
];
}
return xSmooth.map((s) => s[0]);
};
const lats = smoothAxis('lat');
const lngs = smoothAxis('lng');
return accepted.map((p, i) => ({
lat: lats[i],
lng: lngs[i],
logdate: p.logdate,
_ts: p._ts
}));
}
export default function OrdersDetails() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
@@ -502,66 +303,58 @@ export default function OrdersDetails() {
setRiderValue(null);
}, [locationid]);
// ============== Haversine distance calculation for the map route ==============
function calculateDistance(lat1, lon1, lat2, lon2) {
// Haversine sum over consecutive points — total distance for the map's
// route summary. Re-added alongside getdeliverylogs below since
// GET /admin/consignments/:id/logs is now wired up for real.
const calculateTotalDistance = (routeCoordinates) => {
const R = 6371;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLon = (lon2 - lon1) * (Math.PI / 180);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
function calculateTotalDistance(routeCoordinates) {
let totalDistance = 0;
const toRad = (d) => (d * Math.PI) / 180;
let total = 0;
for (let i = 0; i < routeCoordinates.length - 1; i++) {
const { lat: lat1, lng: lon1 } = routeCoordinates[i];
const { lat: lat2, lng: lon2 } = routeCoordinates[i + 1];
totalDistance += calculateDistance(lat1, lon1, lat2, lon2);
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
total += R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
return totalDistance;
}
return total;
};
// GET /admin/consignments/:id/logs is confirmed (jupiter2doormile.md,
// Status: Done) as the GPS trail for one consignment. `id` here is
// `row.deliveryid`, which this page's own data source (fetchDeliveries)
// sets to the underlying bookingid, not a confirmed consignmentid — same
// caveat as Dispatch.js's Compare map. Field names on a log entry aren't
// documented anywhere, so this accepts several likely lat/lng/date key
// spellings defensively rather than assuming one.
const getdeliverylogs = async (id) => {
setLogsLoading(true);
try {
const res = await axios.get(`${process.env.REACT_APP_URL3}/deliveries/getdeliverylogs/?deliveryid=${id}`);
const datas = res.data.details;
if (Array.isArray(datas) && datas.length !== 0) {
// Sort chronologically by logdate
const sorted = datas
.map((r) => {
const ts = r?.logdate ? dayjs(r.logdate) : null;
return {
lat: parseFloat(r?.latitude ?? r?.lat),
lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon),
logdate: r?.logdate,
_ts: ts && ts.isValid() ? ts.valueOf() : Number.MAX_SAFE_INTEGER
};
})
.filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng))
.sort((a, b) => a._ts - b._ts);
const logs = (await getConsignmentLogs(id)) || [];
const points = (Array.isArray(logs) ? logs : [])
.map((r) => ({
lat: parseFloat(r?.latitude ?? r?.lat),
lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon),
logdate: r?.logdate ?? r?.createdat ?? r?.timestamp
}))
.filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng))
.sort((a, b) => new Date(a.logdate || 0) - new Date(b.logdate || 0));
if (sorted.length !== 0) {
setRiderStart(sorted[0].logdate);
setRiderEnd(sorted[sorted.length - 1].logdate);
// Apply Kalman filter
const smoothed = kalmanSmoothGps(sorted);
const coData = smoothed.map((data) => ({ lat: data.lat, lng: data.lng }));
setRiderCoordinates(coData);
calculateTotalDistance(coData);
setMapOpen(true);
} else {
opentoast('No Valid Logs Found', 'error', 2000);
}
} else {
if (points.length === 0) {
setRiderCoordinates([]);
opentoast('No Logs Found ', 'error', 2000);
return;
}
setRiderStart(points[0].logdate);
setRiderEnd(points[points.length - 1].logdate);
const coData = points.map(({ lat, lng }) => ({ lat, lng }));
setRiderCoordinates(coData);
calculateTotalDistance(coData);
setMapOpen(true);
} catch (error) {
console.log('getdeliverylogs', error);
opentoast('No Logs Found ', 'error', 2000);
} finally {
setLogsLoading(false);
}
@@ -667,36 +460,22 @@ export default function OrdersDetails() {
const fetchcount = async () => {
setIsLoading(true);
try {
await axios
.get(
appId == 0
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?fromdate=${startdate}&todate=${enddate}`
: `${
process.env.REACT_APP_URL
}/deliveries/deliverysummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}&userid=${
selectedRider?.userid || 0
}`
)
.then((res) => {
settotal(res.data.details.total);
setPendingLenght(res.data.details.pending);
setAssignLenght(res.data.details.accepted);
setArrivedLenght(res.data.details.arrived);
setPickedLenght(res.data.details.picked);
setActiveLenght(res.data.details.active);
setDeliveredLenght(res.data.details.delivered);
setSkippedLenght(res.data.details.skipped);
setCancelLenght(res.data.details.cancelled);
})
.catch((err) => {
enqueueSnackbar(err.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
});
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) {
console.log(err);
enqueueSnackbar(err.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
} finally {
setIsLoading(false);
}

View File

@@ -1,5 +1,4 @@
import React, { useState, useEffect, useMemo, useRef } from 'react';
import axios from 'axios';
import { useQuery } from '@tanstack/react-query';
// material-ui
@@ -318,36 +317,17 @@ export default function OrdersReport() {
);
}, [filteredRows]);
// ==============================|| per-tenant rider breakdown ||============================== //
const getuserreportsummary = async (tenantId) => {
// No per-tenant or per-location rider-breakdown endpoint in the new API.
const getuserreportsummary = async () => {
setLoading(true);
try {
const res = await axios.get(
`${process.env.REACT_APP_URL}/deliveries/getuserreportsummary/?tenantid=${tenantId}&fromdate=${startdate}&todate=${enddate}`
);
setRidersdata(Array.isArray(res.data?.details) ? res.data.details : []);
} catch (err) {
OpenToast(err?.message || 'Failed to load rider breakdown', 'error', 2000);
setRidersdata([]);
} finally {
setLoading(false);
}
setRidersdata([]);
setLoading(false);
};
// ==============================|| per-location rider breakdown ||============================== //
const getriderlocationsummary = async (locId) => {
const getriderlocationsummary = async () => {
setLoading(true);
try {
const res = await axios.get(
`${process.env.REACT_APP_URL}/deliveries/getriderlocationsummary/?tenantid=${tenantid}&locationid=${locId}&fromdate=${startdate}&todate=${enddate}`
);
setRidersdata(Array.isArray(res.data?.details) ? res.data.details : []);
} catch (err) {
OpenToast(err?.message || 'Failed to load rider breakdown', 'error', 2000);
setRidersdata([]);
} finally {
setLoading(false);
}
setRidersdata([]);
setLoading(false);
};
if (isErrorReports) console.warn('ordersSummary error:', reportsError?.message);

View File

@@ -1,5 +1,4 @@
import React, { useState, useMemo } from 'react';
import axios from 'axios';
import { useQuery } from '@tanstack/react-query';
// material-ui
@@ -59,7 +58,6 @@ import PageHeader from 'components/nearle_components/PageHeader';
import StatCard from 'components/nearle_components/StatCard';
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
import RidersRoutes from './RidersRoutes';
import { OpenToast } from 'components/third-party/OpenToast';
import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard';
// ============================================================================
@@ -242,79 +240,22 @@ export default function RidersSummary() {
);
}, [rows]);
// ==============================|| per-rider tenant breakdown ||============================== //
const fetchTenantSummary = async (riderUserid) => {
// No per-rider tenant-breakdown endpoint in the new API.
const fetchTenantSummary = async () => {
setLoading(true);
try {
const tenantRes = await axios.get(
`${process.env.REACT_APP_URL}/deliveries/getreportsummary/?&fromdate=${startdate}&todate=${enddate}&userid=${riderUserid}`
);
setTenantData(tenantRes.data.details);
} catch (error) {
console.log('tenantRes', error);
} finally {
setLoading(false);
}
setTenantData(null);
setLoading(false);
};
// ==============================|| rider planned route (for map) ||============================== //
// Pulls every delivery the rider was assigned over the page's date range, then
// emits an ordered waypoint list sorted by `step` (the planning sequence). The
// map dialog renders this as the rider's PLANNED route — the path the
// optimizer told them to follow — not their actual GPS trail.
const getuserdeliverylogs = async (userid) => {
// /admin/consignments has no documented per-row schema (lat/lng/step/
// customer fields aren't shown anywhere in the doc), so building a planned
// route from it would be guesswork that risks plotting wrong pins on the
// map. Degrades to an empty route instead.
const getuserdeliverylogs = async () => {
setRouteLoading(true);
try {
const url =
`${process.env.REACT_APP_URL}/deliveries/getdeliveries/` +
`?applocationid=${appId}` +
`&status=all` +
`&fromdate=${startdate}` +
`&todate=${enddate}` +
`&pageno=1` +
`&pagesize=200` +
`&keyword=` +
`&tenantid=` +
`&locationid=` +
`&userid=${userid}`;
const response = await axios.get(url);
const rowsRaw = response?.data?.details || [];
const toNum = (v) => {
const n = Number(v);
return Number.isFinite(n) ? n : null;
};
const planned = rowsRaw
.map((o) => {
const dropLat = toNum(o.droplat ?? o.deliverylat);
const dropLng = toNum(o.droplon ?? o.deliverylong);
const pickLat = toNum(o.pickuplat ?? o.pickuplatitude);
const pickLng = toNum(o.pickuplon ?? o.pickuplong ?? o.picklongitude);
if (dropLat == null || dropLng == null) return null;
return {
step: Number(o.step) || 0,
orderid: o.orderid,
deliveryid: o.deliveryid,
customer: o.deliverycustomer || o.customername || `Order ${o.orderid}`,
address: o.deliveryaddress || o.deliverysuburb || '',
dropLat,
dropLng,
pickLat: pickLat ?? null,
pickLng: pickLng ?? null,
// Expected delivery clock — used as a label under the step pin so
// the operator can sanity-check sequencing without clicking each
// marker.
expectedTime: o.expecteddeliverytime || null
};
})
.filter(Boolean)
.sort((a, b) => a.step - b.step);
setLogDetails(planned);
} catch (err) {
OpenToast(err?.message, 'error', 2000);
setLogDetails([]);
} finally {
setRouteLoading(false);
}
setLogDetails([]);
setRouteLoading(false);
};
// Total Amount sum (preserved from legacy bottom bar).