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

@@ -45,7 +45,7 @@ import { useNavigate } from 'react-router';
import { MdLocationCity } from 'react-icons/md';
import { TbMapPinCode } from 'react-icons/tb';
import { FaLocationDot } from 'react-icons/fa6';
import axios from 'axios';
import { getAdminPricing, getAdminTenant, getTenantCustomers, getTenantLocations, createExpressBooking } from 'pages/api/doormileApi';
import { useTheme } from '@mui/material/styles';
import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete';
import Loader from 'components/Loader';
@@ -127,9 +127,7 @@ const SectionHeader = ({ color, icon, title, subtitle, action }) => (
}}
>
<Stack direction="row" alignItems="center" spacing={1.25}>
<Avatar sx={{ width: 32, height: 32, bgcolor: color, color: '#fff', boxShadow: `0 6px 18px ${ring(color)}` }}>
{icon}
</Avatar>
<Avatar sx={{ width: 32, height: 32, bgcolor: color, color: '#fff', boxShadow: `0 6px 18px ${ring(color)}` }}>{icon}</Avatar>
<Stack>
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 14, lineHeight: 1.1 }}>{title}</Typography>
{subtitle && (
@@ -234,7 +232,6 @@ const Createorder1 = () => {
{ label: '12 Angry Men', year: 1957 }
];
const appId = localStorage.getItem('applocationid');
const navigate = useNavigate();
const [open, setOpen] = useState({});
@@ -340,11 +337,13 @@ const Createorder1 = () => {
const fetchTenantPricing = async () => {
try {
const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${tid}`);
console.log('pricingResponse', pricingResponse.data.details);
setBasePrice(pricingResponse.data.details.baseprice);
setPricePerKm(pricingResponse.data.details.priceperkm);
setMinKm(pricingResponse.data.details.minkm);
const pricing = (await getAdminPricing()) || [];
const match = pricing.find((p) => p.tenantid === tid);
if (match) {
setBasePrice(match.baseprice);
setPricePerKm(match.priceperkm);
setMinKm(match.basedistance);
}
} catch (error) {
console.log('fetchTenantPricing error', error);
}
@@ -450,109 +449,65 @@ const Createorder1 = () => {
}
}, [searchword]);
// ==================================================== || fetchtenantinfo || ====================================================
const fetchtenantinfo = async () => {
setLoading(true);
console.log('tid', tid);
await axios
.get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tid}`)
.then((res) => {
console.log('fetchtenantinfo', res);
if (res.data.status) {
setTenant(res.data.details);
setTenantid(res.data.details.tenantid);
fetchAppAdminTokens();
setSubCatId(res.data.details.subcategoryid);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
try {
const info = await getAdminTenant(tid);
if (info) {
setTenant(info);
setTenantid(info.tenantid);
fetchAppAdminTokens();
setSubCatId(info.subcategoryid);
}
} catch (err) {
console.log(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchtenantinfo();
}, []);
const getsubcategories = async () => {
await axios
.get(`${process.env.REACT_APP_URL}/utils/getsubcategories/?moduleid=6`)
.then((res) => {
console.log('subcateRes', res.data.details);
if (res.data.status && res.data.details) {
setSubCat(res.data.details);
} else {
setSubCat([]);
}
})
.catch((err) => {
console.log(err);
setSubCat([]);
});
};
// No item-subcategory lookup endpoint in the new API.
const getsubcategories = async () => setSubCat([]);
useEffect(() => {
getsubcategories();
}, []);
// ==================================================== || fetchTiming || ====================================================
// No per-zone open/close-hours or geofence-radius endpoint in the new API
// (applocationid/"zones" aren't a resource there at all — see fetchAppLocations
// in pages/api/api.js). Falls back to a fixed 09:00–21:00 delivery window so
// the order-row / time-slot picker below still has slots to initialise from,
// and a generous default radius so the "too far" warning doesn't misfire.
const fetchTiming = async () => {
setLoading(true);
await axios
.get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`)
.then((res) => {
console.log('fetchTiming', res);
const { opentime, closetime, latitude, longitude, radius } = res.data.details[0];
if (res.data.status) {
setAppLocaLat(latitude);
setAppLocaLng(longitude);
setAppLocaRadius(radius);
setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`);
setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`);
console.log('starttime', `${dayjs().format('MM-DD-YYYY')} ${opentime}`);
console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `);
let arr = [];
for (
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`, j = 0;
dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
j++, i = dayjs(i).add(30, 'm')
) {
arr.push(i);
}
console.log('setTimeslotarr', arr);
setTimeslotarr(arr);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
const opentime = '09:00:00';
const closetime = '21:00:00';
setAppLocaLat(0);
setAppLocaLng(0);
setAppLocaRadius(100);
setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`);
setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`);
let arr = [];
for (
let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`;
dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0;
i = dayjs(i).add(30, 'm')
) {
arr.push(i);
}
setTimeslotarr(arr);
setLoading(false);
};
useEffect(() => {
fetchTiming();
}, []);
// =============================================== || fetchAppAdminTokens (via appId) || ===============================================
const fetchAppAdminTokens = async () => {
setLoading(true);
await axios
.get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`)
.then((res) => {
const userfcmtokemArray = res.data.details.applocationadmins.map((admin) => admin.userfcmtokem);
console.log('fetchAppAdminTokens', res);
console.log('userfcmtokemArray', userfcmtokemArray);
if (res.data.status) {
setAdmintoken(userfcmtokemArray);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
// No zone-admin-token lookup endpoint in the new API.
const fetchAppAdminTokens = async () => setAdmintoken([]);
useEffect(() => {
if (starttime && endtime) {
@@ -582,116 +537,47 @@ const Createorder1 = () => {
// =============================================== || createsubmitobj1 (create orders) || ===============================================
const createsubmitobj2 = async () => {
let arr = {};
arr = {
orders: {
applocationid: tenant.applolcationid,
cancellled: '',
categoryid: +tenant.categoryid,
configid: 7,
customerid: isNumChange1 == 0 ? +pickCust.customerid || 0 : 0,
deliveryaddress: dropCust.address || '',
deliverycharge: +totalCharge.toFixed(2) || 0,
deliverycity: dropCust.city || '',
deliverycontactno: dropCust.contactno || '',
deliverycustomer: dropCust.firstname || '',
deliveryid: isNumChange2 == 0 ? +dropCust.customerid || 0 : 0,
deliverylandmark: dropCust.landmark || '',
deliverylat: dropCust.latitude.toString(),
deliverylocation: dropCust.suburb || '',
deliverylocationid: dropCust.deliverylocationid || 0,
deliverylong: dropCust.longitude.toString(),
deliverytime: `${dayjs(startdate).format('YYYY-MM-DD')} ${dayjs(selectedtime.$d).format('HH:mm:ss')}`,
deliverytype: pickCust.customerid !== 0 || dropCust.customerid !== 0 ? 'B' : 'C',
delivered: '',
itemcount: 1,
kms: distance.toString() || 0,
locationid: +tenanatLocoId, //main or branch
moduleid: +tenant.moduleid,
orderamount: +totalCharge.toFixed(2) || 0,
ordercharges: 0.0,
orderdate: dayjs().format('YYYY-MM-DD HH:mm:ss'),
orderheaderid: 0,
orderid: '', //
ordernotes: otherinstructions,
orderstatus: 'created',
ordervalue: +totalCharge.toFixed(2) || 0,
partnerid: tenant.partnerid,
paymentstatus: 1,
paymenttype: 42,
pending: '',
pickupaddress: pickCust.address || '',
pickupcity: pickCust.city || '',
pickupcontactno: pickCust.contactno || '',
pickupcustomer: pickCust.firstname || '',
pickuplandmark: pickCust.landmark || '',
pickuplat: pickCust.latitude.toString(),
pickuplocation: pickCust.suburb || '',
pickuplocationid: pickCust.deliverylocationid || 0,
pickuplong: pickCust.longitude.toString(),
processing: '',
ready: '',
remarks: '',
smsdelivery: isSms,
subcategoryid: +subCatId,
taxamount: 0.0,
tenantid: tenant.tenantid,
tenantuserid: parseInt(localStorage.getItem('userid')),
collectionamt: +collectionamt || 0,
quantity: +quantity || 1
},
pickup: {
address: pickCust.address || '',
applocationid: tenant.applolcationid,
city: pickCust.city || '',
configid: 7,
contactno: pickCust.contactno || '',
customertoken: '',
customerid: isNumChange1 == 0 ? pickCust.customerid || 0 : 0,
devicetype: '',
deviceid: '',
dialcode: '+91',
doorno: pickCust.doorno || '',
email: pickCust.email || '',
firstname: pickCust.firstname || '',
landmark: pickCust.landmark || '',
latitude: pickCust.latitude.toString() || '',
longitude: pickCust.longitude.toString() || '',
locationid: pickCust.deliverylocationid || 0,
postcode: pickCust.postcode || '',
primaryaddress: 1,
profileimage: '',
state: pickCust.state || '',
suburb: pickCust.suburb || '',
tenantid: tenant.tenantid
},
drop: {
address: dropCust.address || '',
applocationid: tenant.applolcationid,
city: dropCust.city || '',
configid: 7,
contactno: dropCust.contactno || '',
customertoken: '',
customerid: isNumChange2 == 0 ? dropCust.customerid || 0 : 0,
devicetype: '',
deviceid: '',
dialcode: '+91',
doorno: dropCust.doorno || '',
email: dropCust.email || '',
firstname: dropCust.firstname || '',
landmark: dropCust.landmark || '',
latitude: dropCust.latitude.toString(),
longitude: dropCust.longitude.toString(),
locationid: dropCust.deliverylocationid || 0,
postcode: dropCust.postcode || '',
primaryaddress: 1,
profileimage: '',
state: dropCust.state || '',
suburb: dropCust.suburb || '',
tenantid: tenant.tenantid
}
// POST /admin/expressbooking's schema is a single flat booking object
// (tenantid, pickuplocationid, 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).
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
}),
customer_phone: dropCust.contactno || '',
customer_name: dropCust.firstname || '',
deliveryaddress: dropCust.address || '',
deliverypincode: dropCust.postcode || '',
deliverycity: dropCust.city || '',
deliverylatitude: Number(dropCust.latitude) || 0,
deliverylongitude: Number(dropCust.longitude) || 0,
service_option: 'Normal',
finalprice: +totalCharge.toFixed(2) || 0,
notes: otherinstructions || '',
parcels: [
{
itemcategory: subCat.find((c) => c.subcategoryid === subCatId)?.categoryname || 'General',
itemdescription: 'Order',
declaredvalue: +totalCharge.toFixed(2) || 0
}
]
};
console.log('createsubmitobj2', arr);
if (!pickCust.firstname) {
@@ -727,29 +613,28 @@ const Createorder1 = () => {
} else if (!setSubCatId) {
opentoast('Choose SubCategory ', 'warning', 2000);
} else {
setLoading(true);
try {
const createRes = await axios.post(`${process.env.REACT_APP_URL2}/orders/createorder`, arr);
// const createRes = await axios.post(`${process.env.REACT_APP_URL}/orders/createorder`, arr);
if (createRes.data.status) {
console.log('createRes', createRes);
const createRes = await createExpressBooking(arr);
if (createRes.success) {
enqueueSnackbar('Order Created Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1000
});
if (admintoken) {
notifyadmin(admintoken);
// sendnotifications();
}
navigate('/nearle/orders');
} else {
opentoast('Something went wrong, Cannot create order', 'warning');
opentoast(createRes.message || 'Something went wrong, Cannot create order', 'warning');
}
setLoading(false);
console.log(createRes);
} 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);
} finally {
setLoading(false);
}
}
};
@@ -774,41 +659,22 @@ const Createorder1 = () => {
const clientdetails = async () => {
setLoading2(true);
try {
let url =
searchCustList == ''
? // ? `${process.env.REACT_APP_URL}/customers/getbytid/?tenantid=${tid}&pageno=1&pagesize=1`
`${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tid}&pageno=1&pagesize=20`
: `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tid}&keyword=${searchCustList}`;
await axios
.get(url)
.then((res) => {
console.log('clientdetails', res.data.details);
if (res.data.status && res.data.details) {
setClientdetail(res.data.details || []);
setCustomerlist(res.data.details || []);
let arr = [];
res.data.details.map((val) => {
arr.push({
label: `${val.firstname} | ${val.contactno}`,
...val
});
});
setClientdetailarr(arr);
} else {
setClientdetail([]);
setCustomerlist([]);
setClientdetailarr([]);
}
setLoading2(false);
})
.catch((err) => {
console.log(err);
setLoading2(false);
opentoast('server error', 'warning');
});
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;
setClientdetail(filtered);
setCustomerlist(filtered);
setClientdetailarr(filtered.map((val) => ({ label: `${val.firstname} | ${val.phone || val.contactno}`, ...val })));
} catch (err) {
console.log(err);
opentoast('server error', 'warning');
} finally {
setLoading2(false);
}
};
@@ -821,74 +687,21 @@ const Createorder1 = () => {
const clientdetailsbusiness = async () => {
setLoading2(true);
try {
await axios
.get(`${process.env.REACT_APP_URL}/customers/getbytid/?tenantid=${tid}&locationid=1`)
.then((res) => {
console.log('clientdetailsbusiness', res.data.details);
if (res.data.status) {
setClientdetail(res.data.details);
// if (!searchword) {
// setClientdetailarr(res.data.details)
// }
let arr = [];
res.data.details.map((val) => {
arr.push({
label: `${val.firstname} | ${val.contactno}`,
...val
});
});
setClientdetailbusinessarr(arr);
}
setLoading2(false);
})
.catch((err) => {
console.log(err);
setLoading2(false);
opentoast('server error', 'warning');
});
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 || ==================================================
const sendnotifications = async () => {
setLoading(true);
await axios
.post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, {
priority: 'high',
registration_ids: admintoken,
data: {
accessid: process.env.REACT_APP_RIDER_ACCESS_ID
},
notification: {
title: 'Nearle Merchant',
body: 'An Order has been placed successfully,kindly process the same',
sound: 'ring'
}
})
.then((res) => {
console.log(res);
if (res.data.message == 'Success') {
enqueueSnackbar('Notification sent Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1000
});
}
setLoading(false);
})
.catch((err) => {
console.log(err);
enqueueSnackbar(err.message, {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 1000
});
setLoading(false);
});
};
// 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).
const sendnotifications = async () => {};
// ============================================= || Address Autocomplete (pick) || =============================================
const handlePickPlaceSelected = (place) => {
setInputValue2(`${place.name}, ${place.formatted_address}`);
@@ -1015,16 +828,11 @@ const Createorder1 = () => {
// ============================================= || gettenantlocations (branches) || =============================================
const gettenantlocations = async () => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${tid}`);
console.log('gettenantlocations', res.data.details);
if (res.data && res.data.details) {
setTenantlocations(res.data.details);
if (res.data.details.length == 1) {
setIsLocation(true);
setTenanatLocoId(res.data.details[0].locationid);
}
} else {
setTenantlocations([]);
const locations = (await getTenantLocations(tid)) || [];
setTenantlocations(locations);
if (locations.length == 1) {
setIsLocation(true);
setTenanatLocoId(locations[0].locationid);
}
} catch (err) {
console.log('gettenantlocations', err);
@@ -1124,9 +932,7 @@ const Createorder1 = () => {
) : (
<Autocomplete
options={tenantLocations || []}
getOptionLabel={(option) =>
option && option.locationname ? `${option.locationname} (${option.suburb || ''})` : ''
}
getOptionLabel={(option) => (option && option.locationname ? `${option.locationname} (${option.suburb || ''})` : '')}
isOptionEqualToValue={(option, value) => option?.locationid === value?.locationid}
PaperComponent={SoftPaper}
sx={{ width: { xs: '100%', sm: 300 } }}
@@ -1926,16 +1732,13 @@ const Createorder1 = () => {
{/* ================================================= || Time || ================================================= */}
<Grid item xs={12} sm={6}>
<SectionCard sx={{ mt: 2, minHeight: 390 }}>
<SectionHeader
color="#f59e0b"
icon={<MdSchedule size={16} />}
title="Schedule"
subtitle="Pickup date & time slot"
/>
<SectionHeader color="#f59e0b" icon={<MdSchedule size={16} />} title="Schedule" subtitle="Pickup date & time slot" />
<Box sx={{ p: { xs: 2, md: 2.5 } }}>
<Grid container>
<Grid item xs={12}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textSecondary, mb: 1 }}>Date</Typography>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textSecondary, mb: 1 }}>
Date
</Typography>
<LocalizationProvider dateAdapter={AdapterDayjs} sx={{}}>
<DatePicker
format="DD-MM-YYYY"
@@ -2006,7 +1809,8 @@ const Createorder1 = () => {
<Stack direction="row" flexWrap="wrap" gap={0.75} useFlexGap>
{timeslotarr.map((val, index) => {
if (
dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0
dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <=
0
) {
const active = dayjs(selectedtime).format('HH:mm') == dayjs(val).format('HH:mm');
return (
@@ -2082,7 +1886,9 @@ const Createorder1 = () => {
getOptionLabel={(option) => `${option.subcategoryname}` || ''}
sx={{ my: 2, zIndex: '100' }}
fullWidth
renderInput={(params) => <TextField {...params} label={subCatName == '' ? tenant.subcategoryname : subCatName} />}
renderInput={(params) => (
<TextField {...params} label={subCatName == '' ? tenant.subcategoryname : subCatName} />
)}
onChange={(event, value, reason) => {
if (value) {
console.log(value);
@@ -2416,14 +2222,16 @@ const Createorder1 = () => {
'&:hover': isUsed
? {}
: {
borderColor: edge(BRAND),
bgcolor: tint(BRAND),
boxShadow: DT.shadowSoft
}
borderColor: edge(BRAND),
bgcolor: tint(BRAND),
boxShadow: DT.shadowSoft
}
}}
>
<Avatar sx={{ width: 36, height: 36, bgcolor: soft(BRAND), color: BRAND, fontWeight: 800 }}>
{String(address.firstname || '?').charAt(0).toUpperCase()}
{String(address.firstname || '?')
.charAt(0)
.toUpperCase()}
</Avatar>
<Stack sx={{ flex: 1, minWidth: 0 }}>
<Stack direction="row" alignItems="center" spacing={0.75} flexWrap="wrap" useFlexGap>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,6 @@ import { useState, useEffect, Fragment, useRef } from 'react';
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import axios from 'axios';
import {
Avatar,
@@ -23,17 +22,14 @@ import {
Dialog,
TableRow,
DialogContent,
DialogTitle,
DialogActions,
Tooltip,
Skeleton,
CircularProgress,
InputBase,
Backdrop,
SpeedDial,
SpeedDialIcon,
SpeedDialAction,
Badge,
TableContainer,
Checkbox
TableContainer
} from '@mui/material';
import {
MdAccessTime,
@@ -51,7 +47,8 @@ import {
MdCalendarMonth,
MdReceiptLong,
MdClear,
MdNotes
MdNotes,
MdMyLocation
} from 'react-icons/md';
import { DeleteOutlined } from '@ant-design/icons';
import Loader from 'components/Loader';
@@ -60,13 +57,8 @@ import CircularLoader from 'components/CircularLoader';
import AiImage from '../../../assets/images/aiImage.png';
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
import { useNavigate, useLocation } from 'react-router-dom';
import {
fetchPercentageData,
createAutomationDeliveries,
cancelMultipleOrder,
getallriders,
fetchorderscount
} from '../../api/api';
import { fetchPercentageData, createAutomationDeliveries, getallriders, fetchorderscount } from '../../api/api';
import { getBookings, cancelBooking, getAdminCustomers, getBookingTrack } from 'pages/api/doormileApi';
// ============================================================================
// Design tokens — shared with the rest of the redesigned operator pages.
@@ -99,29 +91,37 @@ const dtEdge = edge;
const BRAND = '#C01227';
const BRAND_LIGHT = '#D25463';
// Semantic per-row status palette — colors per brand standard:
// green=delivered, amber=pending, blue=created/processing, red=cancelled,
// dark-red=failed, purple=on-hold.
// Semantic per-row status palette, keyed by GET /admin/bookings' real `status`
// enum (lowercased — StatusBadge lowercases before lookup). Only
// pending_pickup and converted_to_consignment have been observed against the
// live API so far; the rest are reasonable guesses at likely sibling states
// with a safe unknown-status fallback in StatusBadge below.
const ROW_STATUS_META = {
created: { label: 'Created', color: '#3b82f6', icon: MdLocalShipping },
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
processing: { label: 'Processing', color: '#3b82f6', icon: MdAccessTime },
modified: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
ready: { label: 'Accepted', color: '#6366f1', icon: MdCheckCircle },
active: { label: 'Picked', color: '#8b5cf6', icon: MdLocalShipping },
onhold: { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff },
closed: { label: 'Closed', color: '#06b6d4', icon: MdCheckCircle },
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
failed: { label: 'Failed', color: '#991b1b', icon: MdCancel },
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
pending_pickup: { label: 'Pending Pickup', color: '#f59e0b', icon: MdHourglassEmpty },
pending_assignment: { label: 'Pending Assignment', color: '#f59e0b', icon: MdHourglassEmpty },
converted_to_consignment: { label: 'Converted', color: '#6366f1', icon: MdCheckCircle },
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
};
// Top-level pill tabs.
const ORDERS_STATUS_TABS = [
{ idx: 0, status: 'created', label: 'Created', color: BRAND, icon: MdLocalShipping, countKey: 'created' }
];
// 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' }];
// 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
// client-side rather than left blank. It's a straight line, not the road
// route the old `kms` field represented — labelled accordingly in the header.
const haversineKm = (lat1, lon1, lat2, lon2) => {
if (![lat1, lon1, lat2, lon2].every(Number.isFinite)) return null;
const R = 6371;
const toRad = (d) => (d * Math.PI) / 180;
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;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
};
const StatusBadge = ({ status }) => {
const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || {
@@ -201,6 +201,16 @@ const Orders = () => {
const [tabstatus, setTabstatus] = useState('Created');
const [currentStatus, setCurrentStatus] = useState('created');
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.
const { data: trackData, isLoading: trackLoading } = useQuery({
queryKey: ['booking-track', trackBookingId],
queryFn: () => getBookingTrack(trackBookingId),
enabled: !!trackBookingId
});
const [orderheaderid, setOrderheaderid] = useState('');
const locationId = 0;
const locoName = 'All Locations';
@@ -213,10 +223,8 @@ const Orders = () => {
// Floating button and Dialog states
const [speedDialOpen, setSpeedDialOpen] = useState(false);
const [multiDeleteDialog, setMultiDeleteDialog] = useState(false);
const [createloader, setCreateloader] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [deliverylist, setDeliverylist] = useState([]);
useEffect(() => {
const handler = setTimeout(() => {
@@ -247,10 +255,7 @@ const Orders = () => {
refetchInterval: 15000
});
const {
data: ordersCountData,
refetch: orderscountRefetch
} = useQuery({
const { data: ordersCountData, refetch: orderscountRefetch } = useQuery({
queryKey: ['ordersCount', locationId, startdate, enddate, currentStatus, tid, locationId],
queryFn: fetchorderscount,
refetchOnMount: true,
@@ -258,47 +263,69 @@ const Orders = () => {
refetchInterval: 15000
});
const {
data: autoRiders
} = useQuery({
const { data: autoRiders } = useQuery({
queryKey: ['getallriders'],
queryFn: getallriders,
refetchOnMount: true,
refetchOnWindowFocus: true
});
const cancelorder = async () => {
await axios
.put(`${process.env.REACT_APP_URL}/orders/updateorder`, {
orderheaderid: orderheaderid,
orderstatus: 'cancelled',
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss')
})
.then((res) => {
if (res.data.status) {
enqueueSnackbar('Order Cancelled Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
refetchOrders();
orderscountRefetch();
percentagedataRefetch();
setCancelOpen(false);
}
})
.catch((err) => {
console.log(err);
// GET /admin/bookings only carries `appcustomerid` — no name/phone on the
// booking itself — so customer display fields are joined client-side
// against GET /admin/customers. Response field names for that endpoint
// aren't documented; firstname/phone are guessed by symmetry with
// tenantcustomers and fall back to showing the raw id.
const { data: customerMap } = useQuery({
queryKey: ['adminCustomersMap'],
queryFn: async () => {
const customers = (await getAdminCustomers()) || [];
const map = new Map();
customers.forEach((c) => {
const id = c.appcustomerid ?? c.customerid ?? c.id;
if (id != null) map.set(id, c);
});
return map;
},
staleTime: 5 * 60 * 1000
});
const cancelorder = async () => {
try {
const res = await cancelBooking(orderheaderid);
if (res.success) {
enqueueSnackbar('Order Cancelled Successfully', {
variant: 'success',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
refetchOrders();
orderscountRefetch();
percentagedataRefetch();
setCancelOpen(false);
} else {
enqueueSnackbar(res.message || 'Failed to cancel order', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
} catch (err) {
console.log(err);
enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to cancel order', {
variant: 'error',
anchorOrigin: { vertical: 'top', horizontal: 'right' },
autoHideDuration: 2000
});
}
};
// 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 res = await axios.get(
`${process.env.REACT_APP_URL}/orders/tenant/getorders/?tenantid=${tid}&locationid=${locationId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${debouncedSearch}`
);
const data = (await getBookings(pageParam, rowsPerPage)) || [];
return {
data: res.data.details,
nextPage: res.data.details.length === rowsPerPage ? pageParam + 1 : undefined
data,
nextPage: data.length === rowsPerPage ? pageParam + 1 : undefined
};
};
@@ -352,7 +379,11 @@ const Orders = () => {
const createDeliveryMutation = useMutation({
mutationFn: createAutomationDeliveries,
onSuccess: (data, variables) => {
enqueueSnackbar('Orders Optimised Successfully', { variant: 'success', autoHideDuration: 2000, anchorOrigin: { vertical: 'top', horizontal: 'right' } });
enqueueSnackbar('Orders Optimised Successfully', {
variant: 'success',
autoHideDuration: 2000,
anchorOrigin: { vertical: 'top', horizontal: 'right' }
});
orderscountRefetch();
refetchOrders();
setCreateloader(false);
@@ -379,43 +410,37 @@ const Orders = () => {
}
});
const cancelMultipleOrderMutation = useMutation({
mutationFn: cancelMultipleOrder,
onSuccess: (data) => {
if (data.status) {
setMultiDeleteDialog(false);
enqueueSnackbar('Orders Cancelled Successfully', { variant: 'success', autoHideDuration: 2000 });
refetchOrders();
orderscountRefetch();
percentagedataRefetch();
setDeliverylist([]);
}
},
onError: (error) => {
enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 4000 });
}
});
const handleCreateDelivery = async () => {
if (rows.length === 0) return;
setIsLoading(true);
setCreateloader(true);
const deliveryData = rows.map((val) => ({
...val,
deliveryid: 0,
deliverydate: dayjs(val.deliverydate).utc().format('YYYY-MM-DD HH:mm:ss'),
assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
orderstatus: 'pending',
orderamount: val.deliverycharge,
droplat: val.deliverylat,
droplon: val.deliverylong,
pickuplat: val.pickuplat,
pickuplon: val.pickuplong,
ordernotes: val.ordernotes,
deliverycharges: val.deliverycharge,
pickuplocation: val.pickupsuburb,
deliverylocation: val.deliverysuburb
}));
// The optimiser service (routes.workolik.com) itself is untouched, but the
// fields sourced here have to match GET /admin/bookings' real shape —
// deliverylat/pickuplat/deliverycharge/etc. don't exist on a booking
// 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) => {
const charge = val.serviceoptions?.[0]?.estimatedprice;
return {
...val,
deliveryid: 0,
deliverydate: dayjs(val.serviceoptions?.[0]?.estimateddeliveryat || val.createdat)
.utc()
.format('YYYY-MM-DD HH:mm:ss'),
assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
orderstatus: 'pending',
orderamount: charge,
droplat: val.deliverylatitude,
droplon: val.deliverylongitude,
pickuplat: val.pickuplatitude,
pickuplon: val.pickuplongitude,
ordernotes: val.notes,
deliverycharges: charge,
pickuplocation: val.pickupaddress,
deliverylocation: val.deliveryaddress
};
});
createDeliveryMutation.mutate({
deliveries: deliveryData,
@@ -427,8 +452,22 @@ const Orders = () => {
// KPI tile definitions.
const kpiCards = [
{ key: 'created', label: 'Created Orders', color: BRAND, icon: MdLocalShipping, value: percentageData?.created, percentage: percentageData?.percentage1 },
{ key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdHourglassEmpty, value: percentageData?.uncoveredOrders, percentage: percentageData?.percentage2 }
{
key: 'created',
label: 'Created Orders',
color: BRAND,
icon: MdLocalShipping,
value: percentageData?.created,
percentage: percentageData?.percentage1
},
{
key: 'pending',
label: 'Pending Orders',
color: '#f59e0b',
icon: MdHourglassEmpty,
value: percentageData?.uncoveredOrders,
percentage: percentageData?.percentage2
}
];
return (
@@ -532,14 +571,9 @@ const Orders = () => {
</Stack>
</Stack>
</Stack>
</Stack>
</Paper>
{/* ============================================= || Status Tabs + Search (compact) || ============================================= */}
<Paper
elevation={0}
@@ -556,13 +590,7 @@ const Orders = () => {
background: '#fff'
}}
>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
gap={1.5}
sx={{ flexWrap: 'wrap-reverse' }}
>
<Stack direction="row" alignItems="center" justifyContent="space-between" gap={1.5} sx={{ flexWrap: 'wrap-reverse' }}>
<Stack
direction="row"
spacing={0.75}
@@ -640,7 +668,6 @@ const Orders = () => {
);
})}
</Stack>
</Stack>
</Paper>
@@ -692,16 +719,16 @@ const Orders = () => {
}}
>
<TableCell>#</TableCell>
<TableCell>Order Location</TableCell>
<TableCell>Order</TableCell>
<TableCell>Pickup</TableCell>
<TableCell>Drop</TableCell>
<TableCell align="center">Qty</TableCell>
<TableCell align="right">COD</TableCell>
<TableCell align="center">Kms</TableCell>
<TableCell align="center">Kms (approx)</TableCell>
<TableCell align="right">Charges</TableCell>
<TableCell>Notes</TableCell>
<TableCell>Status</TableCell>
{currentStatus === 'created' && <TableCell align="right">Actions</TableCell>}
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
@@ -710,7 +737,7 @@ const Orders = () => {
rows.length === 0 &&
Array.from({ length: 10 }).map((_, idx) => (
<TableRow key={`sk-${idx}`}>
{Array.from({ length: currentStatus === 'created' ? 12 : 11 }).map((__, ci) => (
{Array.from({ length: 11 }).map((__, ci) => (
<TableCell key={ci} sx={{ borderBottom: `1px solid ${DT.divider}`, py: 0.625, px: 1 }}>
<Skeleton animation="wave" height={20} />
</TableCell>
@@ -720,7 +747,7 @@ const Orders = () => {
{!isLoadingGetOrders && rows.length === 0 && (
<TableRow>
<TableCell colSpan={currentStatus === 'created' ? 12 : 11} sx={{ py: 7, borderBottom: 'none' }}>
<TableCell colSpan={11} sx={{ py: 7, borderBottom: 'none' }}>
<Stack alignItems="center" spacing={1.25}>
<Avatar
variant="rounded"
@@ -734,9 +761,7 @@ const Orders = () => {
>
<MdLocalShipping size={26} />
</Avatar>
<Typography sx={{ fontWeight: 700, color: DT.textPrimary, fontSize: 14 }}>
No {currentStatus} orders
</Typography>
<Typography sx={{ fontWeight: 700, color: DT.textPrimary, fontSize: 14 }}>No {currentStatus} orders</Typography>
<Typography sx={{ color: DT.textSecondary, fontSize: 12 }}>
{searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'}
</Typography>
@@ -765,20 +790,13 @@ const Orders = () => {
)}
{rows.map((row, index) => {
const isItemSelected = !!deliverylist.find((res) => res.orderheaderid === row.orderheaderid);
const handleCheckbox = (e) => {
if (e.target.checked) {
setDeliverylist((prev) => [...prev, { ...row, sno: prev.length + 1 }]);
} else {
setDeliverylist((prev) =>
prev.filter((item) => item.orderheaderid !== row.orderheaderid).map((item, i) => ({ ...item, sno: i + 1 }))
);
}
};
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);
return (
<TableRow
key={`${row.orderheaderid}-${index}`}
key={`${row.bookingid}-${index}`}
sx={{
cursor: 'pointer',
transition: 'background-color 0.12s, box-shadow 0.12s',
@@ -796,34 +814,24 @@ const Orders = () => {
}}
>
<TableCell>
<Typography sx={{ fontWeight: 700, color: DT.textSecondary }}>
{page * rowsPerPage + index + 1}
</Typography>
<Typography sx={{ fontWeight: 700, color: DT.textSecondary }}>{page * rowsPerPage + index + 1}</Typography>
</TableCell>
<TableCell>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.locationname}
{row.locationsuburb && ` - ${row.locationsuburb}`}
{row.bookingno || `#${row.bookingid}`}
</Typography>
<Tooltip title="Order Id">
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
{row.orderid}
</Typography>
</Tooltip>
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mt: 0.125 }}>
<MdAccessTime size={10} style={{ color: DT.textMuted, flexShrink: 0 }} />
{(() => {
const dateObj = row.pickupslot && dayjs(row.pickupslot).isValid()
? dayjs(row.pickupslot)
: dayjs(row.deliverydate || row.orderdate);
const dateObj = dayjs(row.createdat);
return (
<>
<Typography sx={{ fontSize: 10.5, color: DT.textSecondary, fontWeight: 700 }} noWrap>
{dateObj.format('hh:mm A')}
{dateObj.isValid() ? dateObj.format('hh:mm A') : '—'}
</Typography>
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600 }} noWrap>
· {dateObj.format('DD MMM YY')}
{dateObj.isValid() && `· ${dateObj.format('DD MMM YY')}`}
</Typography>
</>
);
@@ -833,70 +841,85 @@ const Orders = () => {
<TableCell>
<Stack direction="column">
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.pickupcustomer}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.pickupcontactno}
</Typography>
<Tooltip title={row.pickupaddress}>
<Typography variant="caption" sx={{ color: DT.textMuted }} noWrap>
{row.pickupsuburb || (row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—')}
<Tooltip title={row.pickupaddress || ''}>
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—'}
</Typography>
</Tooltip>
<Typography variant="caption" sx={{ color: DT.textMuted }} noWrap>
{row.pickuppincode || ''}
</Typography>
</Stack>
</TableCell>
<TableCell>
<Stack direction="column">
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
{row.deliverycustomer}
{customer?.firstname || customer?.name || `Customer #${row.appcustomerid ?? '—'}`}
</Typography>
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
{row.deliverycontactno}
{customer?.phone || customer?.contactno || ''}
</Typography>
<Tooltip title={row.deliveryaddress}>
<Tooltip title={row.deliveryaddress || ''}>
<Typography variant="caption" sx={{ color: DT.textMuted }} noWrap>
{row.deliverysuburb ||
(row.deliveryaddress?.length > 20 ? `${row.deliveryaddress.slice(0, 20)}…` : row.deliveryaddress || '—')}
{row.deliveryaddress?.length > 20 ? `${row.deliveryaddress.slice(0, 20)}…` : row.deliveryaddress || '—'}
</Typography>
</Tooltip>
</Stack>
</TableCell>
<TableCell align="center">
<MetricCell value={row.quantity} color="#0ea5e9" icon={<MdInventory2 size={11} />} />
<MetricCell value={row.parcels?.length || 0} color="#0ea5e9" icon={<MdInventory2 size={11} />} />
</TableCell>
<TableCell align="right">
<MetricCell value={row.collectionamt} color="#10b981" icon={<MdCurrencyRupee size={11} />} isMoney />
<Tooltip title="Not available on the Doormile Express API yet">
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 700 }}>
—
</Typography>
</Tooltip>
</TableCell>
<TableCell align="center">
<MetricCell value={row.kms} color="#f59e0b" icon={<MdStraighten size={11} />} />
{kms != null ? (
<Tooltip title="Straight-line distance — pickup/delivery road distance isn't returned by the API">
<span>
<MetricCell value={kms.toFixed(1)} color="#f59e0b" icon={<MdStraighten size={11} />} />
</span>
</Tooltip>
) : (
<MetricCell value={null} color="#f59e0b" icon={<MdStraighten size={11} />} />
)}
</TableCell>
<TableCell align="right">
<MetricCell value={row.deliverycharge} color={BRAND} icon={<MdCurrencyRupee size={11} />} isMoney />
<MetricCell
value={row.serviceoptions?.[0]?.estimatedprice}
color={BRAND}
icon={<MdCurrencyRupee size={11} />}
isMoney
/>
</TableCell>
<TableCell>
{row.ordernotes ? (
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
<MdNotes size={12} style={{ color: DT.textMuted, flexShrink: 0 }} />
<Typography
variant="caption"
sx={{
maxWidth: 140,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
fontWeight: 600
}}
>
{row.ordernotes}
</Typography>
</Stack>
{row.notes ? (
<Tooltip title={row.notes}>
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
<MdNotes size={12} style={{ color: DT.textMuted, flexShrink: 0 }} />
<Typography
variant="caption"
sx={{
maxWidth: 140,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
fontWeight: 600
}}
>
{row.notes}
</Typography>
</Stack>
</Tooltip>
) : (
<Typography variant="caption" sx={{ color: DT.textMuted }}>
—
@@ -905,18 +928,40 @@ const Orders = () => {
</TableCell>
<TableCell>
<StatusBadge status={row.orderstatus} />
<StatusBadge status={row.status} />
</TableCell>
{currentStatus === 'created' && (
<TableCell align="right">
{row.orderstatus === 'created' && (
<TableCell align="right">
<Stack direction="row" spacing={0.5} justifyContent="flex-end">
<Tooltip title="Track">
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
setTrackBookingId(row.bookingid);
}}
sx={{
bgcolor: tint('#0ea5e9'),
border: `1px solid ${edge('#0ea5e9')}`,
color: '#0ea5e9',
borderRadius: 999,
p: 0.75,
'&:hover': {
bgcolor: soft('#0ea5e9'),
borderColor: '#0ea5e9'
}
}}
>
<MdMyLocation size={14} />
</IconButton>
</Tooltip>
{currentStatus === 'created' && isCancellable && (
<Tooltip title="Cancel Order">
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
setOrderheaderid(row.orderheaderid);
setOrderheaderid(row.bookingid);
setCancelOpen(true);
}}
sx={{
@@ -935,29 +980,20 @@ const Orders = () => {
</IconButton>
</Tooltip>
)}
</TableCell>
)}
</Stack>
</TableCell>
</TableRow>
);
})}
{rows.length !== 0 && (
<TableRow sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}>
<TableCell colSpan={currentStatus === 'created' ? 12 : 11} sx={{ borderBottom: 'none', py: 1, bgcolor: DT.surfaceAlt }}>
<Stack
ref={loadMoreRef}
direction="row"
alignItems="center"
justifyContent="center"
spacing={1}
sx={{ minHeight: 32 }}
>
<TableCell colSpan={11} sx={{ borderBottom: 'none', py: 1, bgcolor: DT.surfaceAlt }}>
<Stack ref={loadMoreRef} direction="row" alignItems="center" justifyContent="center" spacing={1} sx={{ minHeight: 32 }}>
{isFetchingNextPage || hasNextPage ? (
<>
<CircularProgress size={14} sx={{ color: BRAND }} />
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 700 }}>
Loading more orders…
</Typography>
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 700 }}>Loading more orders…</Typography>
</>
) : (
<Typography sx={{ fontSize: 11.5, color: DT.textMuted, fontWeight: 700, letterSpacing: 0.3 }}>
@@ -1033,6 +1069,38 @@ const Orders = () => {
</DialogContent>
</Dialog>
{/* ============================================= || Track Order || ============================================= */}
<Dialog open={!!trackBookingId} onClose={() => setTrackBookingId(null)} maxWidth="xs" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}>
<DialogTitle sx={{ fontWeight: 700 }}>Track Order</DialogTitle>
<DialogContent dividers>
{trackLoading ? (
<Stack alignItems="center" sx={{ py: 3 }}>
<CircularProgress size={28} />
</Stack>
) : trackData ? (
<Stack spacing={1.5}>
{Object.entries(trackData).map(([key, value]) => (
<Stack key={key} direction="row" justifyContent="space-between" spacing={2}>
<Typography variant="caption" sx={{ color: DT.textMuted, textTransform: 'uppercase', fontWeight: 700 }}>
{key}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'right', wordBreak: 'break-word' }}>
{typeof value === 'object' ? JSON.stringify(value) : String(value ?? '—')}
</Typography>
</Stack>
))}
</Stack>
) : (
<Typography variant="body2" sx={{ color: DT.textSecondary, textAlign: 'center', py: 3 }}>
No tracking data available for this order yet.
</Typography>
)}
</DialogContent>
<DialogActions>
<Button onClick={() => setTrackBookingId(null)}>Close</Button>
</DialogActions>
</Dialog>
{/* ============================================= || Date Filter || ============================================= */}
<DateFilterDialog
open={dateOpen}
@@ -1043,67 +1111,6 @@ const Orders = () => {
setDatestatus(label);
}}
/>
{/* ============================================= || Cancel Multiple Orders Dialog || ============================================= */}
<Dialog open={multiDeleteDialog} onClose={() => setMultiDeleteDialog(false)} maxWidth="xs" PaperProps={{ sx: { borderRadius: 3 } }}>
<Box
sx={{
p: 2.5,
background: `linear-gradient(135deg, ${tint('#ef4444')} 0%, ${tint('#f59e0b')} 100%)`,
borderBottom: `1px solid ${DT.borderSubtle}`
}}
>
<Stack direction="row" alignItems="center" spacing={1.5}>
<Avatar sx={{ bgcolor: '#ef4444', color: '#fff', width: 40, height: 40 }}>
<MdDeleteOutline size={20} />
</Avatar>
<Typography variant="h5" sx={{ fontWeight: 800, color: DT.textPrimary }}>
Cancel Selected Orders
</Typography>
</Stack>
</Box>
<DialogContent sx={{ pt: 3 }}>
<Stack alignItems="center" spacing={3}>
<Typography variant="body1" align="center" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
Are you sure you want to cancel the {deliverylist.length} selected orders? This action cannot be undone.
</Typography>
<Stack direction="row" spacing={1.5} sx={{ width: 1 }}>
<Button
fullWidth
onClick={() => setMultiDeleteDialog(false)}
variant="outlined"
sx={{
borderRadius: 999,
py: 1,
borderColor: DT.borderSubtle,
color: DT.textSecondary,
fontWeight: 700,
'&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
}}
>
No
</Button>
<Button
fullWidth
variant="contained"
onClick={() => cancelMultipleOrderMutation.mutate(deliverylist)}
autoFocus
sx={{
borderRadius: 999,
py: 1,
bgcolor: '#ef4444',
fontWeight: 700,
boxShadow: `0 6px 18px ${ring('#ef4444')}`,
'&:hover': { bgcolor: '#dc2626' }
}}
>
Yes, Cancel
</Button>
</Stack>
</Stack>
</DialogContent>
</Dialog>
</Fragment>
);
};