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:
@@ -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
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user