diff --git a/src/menu-items/nearle.js b/src/menu-items/nearle.js
index 05822ca..b0554ec 100644
--- a/src/menu-items/nearle.js
+++ b/src/menu-items/nearle.js
@@ -58,6 +58,13 @@ const nearle = {
icon: icons.FileDoneOutlined,
type: 'group',
children: [
+ {
+ id: 'dashboard',
+ title: ,
+ type: 'item',
+ url: '/nearle/dashboard',
+ icon: icons.DashboardOutlined
+ },
{
id: 'dispatch',
title: ,
@@ -107,6 +114,13 @@ const nearle = {
type: 'item',
url: '/nearle/riders',
icon: DirectionsBikeOutlinedIcon
+ },
+ {
+ id: 'hubs',
+ title: ,
+ type: 'item',
+ url: '/nearle/hubs',
+ icon: icons.DeploymentUnitOutlined
}
]
};
diff --git a/src/pages/api/api.js b/src/pages/api/api.js
index e37aa4f..679c5dd 100644
--- a/src/pages/api/api.js
+++ b/src/pages/api/api.js
@@ -34,6 +34,21 @@ export const fetchAppLocations = async () => {
return [...hubs, { locationname: 'All', applocationid: 0 }];
};
+// ==============================|| fetchHubs / createHub / updateHub (hubs) ||============================== //
+
+export const fetchHubs = async () => {
+ const res = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
+ return res.data?.data || [];
+};
+export const createHub = async (body) => {
+ const res = await axios.post(`${process.env.REACT_APP_URL}/admin/hubs`, body);
+ return res.data;
+};
+export const updateHub = async (id, body) => {
+ const res = await axios.patch(`${process.env.REACT_APP_URL}/admin/hubs/${id}`, body);
+ return res.data;
+};
+
// ==============================|| fetchPercentageData (orders) ||============================== //
export const fetchPercentageData = async ({ queryKey }) => {
@@ -128,33 +143,25 @@ export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
};
// ==============================|| fetchPaymentType (orders) ||============================== //
-
-export const fetchPaymentType = async () => {
- const { data } = await axios.get(`${process.env.REACT_APP_URL}/utils/getapptypes/?tag=paymentmode`);
- return data.details.map((val) => ({
- ...val,
- label: val.typename
- }));
-};
+// No Doormile payment-types endpoint exists yet β static defaults.
+export const fetchPaymentType = async () => [
+ { apptypeid: 1, typename: 'Cash', label: 'Cash' },
+ { apptypeid: 2, typename: 'Online', label: 'Online' },
+ { apptypeid: 3, typename: 'COD', label: 'COD' }
+];
// ==============================|| fetchRidersList (orders) ||============================== //
-export const fetchRidersList = async ({ queryKey }) => {
- try {
- const [, appId] = queryKey; // Extract appId from queryKey
- const { data } = await axios.get(`${process.env.REACT_APP_URL}/partners/getriders/?applocationid=${appId}`);
- console.log('data', data);
- const response = data?.details
- ? data?.details.map((val) => ({
- ...val,
- label: `${val.firstname} ${val.lastname} | ${val.contactno}`
- }))
- : [];
- return response;
- } catch (err) {
- OpenToast(err.message, 'error', 2000);
- throw err; // π₯ REQUIRED
- }
+export const fetchRidersList = async () => {
+ const res = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
+ return (res.data?.data || []).map((m) => ({
+ ...m,
+ userid: m.userid,
+ label: `${m.displayname} | ${m.phone}`,
+ firstname: m.displayname,
+ lastname: '',
+ contactno: m.phone
+ }));
};
// ==============================|| Doormile assignment (new) ||============================== //
@@ -284,13 +291,8 @@ export const notifyRider = async () => ({ success: true });
// ==============================|| cancelOrder (orders) ||============================== //
-export const cancelOrder = async (orderheaderid) => {
- const response = await axios.put(`${process.env.REACT_APP_URL}/orders/updateorder`, {
- orderheaderid: orderheaderid,
- orderstatus: 'cancelled',
- cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss')
- });
-
+export const cancelOrder = async (bookingid) => {
+ const response = await axios.patch(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}/status`, { status: 'Cancelled' });
return response.data;
};
// ==============================|| cancelMultipleOrder (orders) ||============================== //
@@ -337,81 +339,74 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
};
// ==============================|| fetchPercentageAPI (deliveries) ||============================== //
-
-export const fetchPercentageAPI = async (appId) => {
- const url = `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}`;
- const response = await axios.get(url);
-
- const data = response.data.details;
+// UNVERIFIED: status param + pagesize=1-for-count-only pattern hasn't been
+// confirmed against the real backend the way bookings/clients/milers were β
+// implemented per explicit instruction, flag if it 404s/403s like
+// /admin/customers did.
+export const fetchPercentageAPI = async () => {
+ const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Picked_Up', 'Delivered', 'Cancelled'];
+ const results = await Promise.all(
+ statuses.map((s) => axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: s, pagesize: 1 } }))
+ );
+ const [pending, assigned, picked, delivered, cancelled] = results.map((r) => r.data?.total || 0);
+ const total = pending + assigned + picked + delivered + cancelled;
return {
- coveredOrders: data.delivered.toString(),
- cancelledOrders: data.cancelled.toString(),
- uncoveredOrders: data.pending.toString(),
- assignedOrders: data.accepted.toString(),
- createdOrders: data.created.toString(),
- closedOrders: data.delivered.toString(),
- pickedOrders: data.picked.toString(),
- percentage1: (Math.round((data.pending / data.total) * 100) || 0).toString(),
- percentage2: (Math.round((data.accepted / data.total) * 100) || 0).toString(),
- percentage3: (Math.round((data.picked / data.total) * 100) || 0).toString(),
- percentage4: (Math.round((data.delivered / data.total) * 100) || 0).toString()
+ coveredOrders: delivered.toString(),
+ cancelledOrders: cancelled.toString(),
+ uncoveredOrders: pending.toString(),
+ assignedOrders: assigned.toString(),
+ createdOrders: total.toString(),
+ closedOrders: delivered.toString(),
+ pickedOrders: picked.toString(),
+ total: total.toString(),
+ percentage1: (Math.round((pending / total) * 100) || 0).toString(),
+ percentage2: (Math.round((assigned / total) * 100) || 0).toString(),
+ percentage3: (Math.round((picked / total) * 100) || 0).toString(),
+ percentage4: (Math.round((delivered / total) * 100) || 0).toString()
};
};
// ==============================|| fetchCountAPI (deliveries) ||============================== //
-export const fetchCountAPI = async (appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) => {
- const url =
- appId == 0
- ? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?appuserid=${userid}&fromdate=${startdate}&todate=${enddate}`
- : `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
- const response = await axios.get(url);
- const data = response.data.details;
+export const fetchCountAPI = async () => {
+ const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Pickup_Scheduled', 'At_Customer', 'Picked_Up', 'At_Hub', 'Delivered', 'Cancelled'];
+ const results = await Promise.all(
+ statuses.map((s) => axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: s, pagesize: 1 } }))
+ );
+ const [pending, assigned, scheduled, atcustomer, picked, athub, delivered, cancelled] = results.map((r) => r.data?.total || 0);
return {
- total: data.total,
- uncoveredLength: data.pending,
- assignedLength: data.accepted,
- arrivedLength: data.arrived,
- pickedLength: data.picked,
- activeLength: data.active,
- coveredLength: data.delivered,
- cancelLength: data.cancelled,
- skippedLength: data.skipped
+ total: pending + assigned + scheduled + atcustomer + picked + athub + delivered + cancelled,
+ uncoveredLength: pending,
+ assignedLength: assigned,
+ arrivedLength: scheduled,
+ pickedLength: picked,
+ activeLength: atcustomer + athub,
+ coveredLength: delivered,
+ cancelLength: cancelled,
+ skippedLength: 0
};
};
// ==============================|| cancelDeliveryAPI (deliveries) ||============================== //
-export const cancelDeliveryAPI = async (selectedRow, cancelFeed) => {
- const payload = {
- deliveryid: selectedRow.deliveryid,
- orderheaderid: selectedRow.orderheaderid,
- orderstatus: 'cancelled',
- canceltime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
- feedback: cancelFeed
- };
- const response = await axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, payload);
+export const cancelDeliveryAPI = async (selectedRow) => {
+ const response = await axios.patch(`${process.env.REACT_APP_URL}/admin/bookings/${selectedRow.bookingid}/status`, {
+ status: 'Cancelled'
+ });
return response.data;
};
// ==============================|| getorderdetails (deliveries) ||============================== //
-export const getorderdetails = async (orderHeaderid) => {
- const response = await axios.get(`${process.env.REACT_APP_URL}/orders/getorderdetails?orderheaderid=${orderHeaderid}`);
+export const getorderdetails = async (bookingid) => {
+ const response = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}`);
return response.data;
};
// ==============================|| changeRiderAPI (deliveries) ||============================== //
export const changeRiderAPI = async (selectedRider, selectedRow) => {
- console.log('selectedRider', selectedRider);
- console.log('selectedRow', selectedRow);
-
- return axios.put(`${process.env.REACT_APP_URL}/deliveries/updatedelivery`, {
- userid: selectedRider.userid,
- deliveryid: selectedRow.deliveryid,
- orderheaderid: selectedRow.orderheaderid,
- orderstatus: 'pending',
- assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss')
+ return axios.post(`${process.env.REACT_APP_URL}/hub/bookings/${selectedRow.bookingid}/assign-miler`, {
+ mileruserid: selectedRider.userid
});
};
// ==============================|| updateDeliveryAPI (deliveries) ||============================== //
@@ -465,30 +460,18 @@ export const gettenantsummary = async () => {
};
// ==============================|| getpricinglist (tenants) ||============================== //
-export const getpricinglist = async ({ queryKey }) => {
- const [, appId] = queryKey;
+export const getpricinglist = async () => {
try {
- const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/getpricinglist/?moduleid=6&applocationid=${appId}`);
- return response.data.summary; // return only data, keep it clean
+ const response = await axios.get(`${process.env.REACT_APP_URL}/admin/pricing`);
+ return response.data?.data || [];
} catch (err) {
- const message = err.response?.data?.message || err.message || 'Something went wrong';
- OpenToast(message);
- return null; // return null for failure
+ OpenToast(err.message, 'error', 2000);
+ return [];
}
};
// ==============================|| getallpricing (clientPricing) ||============================== //
-export const getallpricing = async ({ queryKey }) => {
- const [, appId] = queryKey;
- try {
- const response = await axios.get(`${process.env.REACT_APP_URL}/utils/getallpricing/?applocationid=${appId}`);
- return response.data.details || [];
- } catch (err) {
- const message = err.response?.data?.message || err.message || 'Something went wrong';
- OpenToast(message);
- return [];
- }
-};
+export const getallpricing = getpricinglist;
// ==============================|| getcustomersummary (customers) ||============================== //
// No dedicated Doormile summary endpoint has been specified for customers β
@@ -621,35 +604,26 @@ export const fetchorderdetails = async ({ queryKey }) => {
};
// ==============================|| fetchLocations (orders summary))||============================== //
+// Not needed as a separate concept in Doormile β returns the hub list instead.
export const fetchLocations = async () => {
- const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getpartners`);
- const updatedLocations = [
- ...response.data.details,
- { partnername: 'All', partnerid: -1 } // Add your new object here
+ const response = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
+ return [
+ ...(response.data?.data || []).map((h) => ({ ...h, partnername: h.hubname, partnerid: h.hubid })),
+ { partnername: 'All', partnerid: -1 }
];
- console.log('fetchLocations', updatedLocations);
- return updatedLocations;
};
// ==============================|| fetchRidersLogs (RiderLogs)||============================== //
-
-export const fetchRidersLogs = async ({ queryKey }) => {
- const [appId, startdate, riderSearch = ''] = queryKey;
- const riderLogsResponse = await axios.get(
- `${process.env.REACT_APP_URL2}/partners/getriderlogs/?applocationid=${appId}&fromdate=${startdate || ''}&todate=${startdate}&keyword=${riderSearch || ''
- } `
- );
- console.log('fetchRidersLogs', riderLogsResponse.data.details);
- return riderLogsResponse.data.details;
-};
+// No Doormile equivalent yet (real-time miler location stream lands with EMQX).
+export const fetchRidersLogs = async () => [];
// ==============================|| getusers (viewProfile)||============================== //
export const getusers = async () => {
try {
- const res = await axios.get(`${process.env.REACT_APP_URL}/users/getusers/?configid=9&userid=${userid}`);
- return res.data.details;
+ const res = await axios.get(`${process.env.REACT_APP_URL}/admin/users`);
+ return res.data?.data || [];
} catch (err) {
- console.log('getusers', err.message);
+ return [];
}
};
diff --git a/src/pages/nearle/bookings/BookingDetail.js b/src/pages/nearle/bookings/BookingDetail.js
new file mode 100644
index 0000000..5537e4b
--- /dev/null
+++ b/src/pages/nearle/bookings/BookingDetail.js
@@ -0,0 +1,308 @@
+import { useParams, useNavigate } from 'react-router-dom';
+import { Avatar, Box, Button, Chip, Grid, Paper, Stack, Step, StepLabel, Stepper, Typography, useMediaQuery } from '@mui/material';
+import { useTheme } from '@mui/material/styles';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import {
+ MdArrowBack,
+ MdLocalShipping,
+ MdPerson,
+ MdPhone,
+ MdLocationOn,
+ MdTwoWheeler,
+ MdStar,
+ MdOutlineSmartToy,
+ MdOutlineAssignmentInd,
+ MdOutlineCancel
+} from 'react-icons/md';
+import Loader from 'components/Loader';
+import { OpenToast } from 'components/third-party/OpenToast';
+import { getorderdetails, autoAssignBooking, cancelOrder } from 'pages/api/api';
+
+const DT = {
+ radiusCard: 16,
+ textPrimary: '#0f172a',
+ textSecondary: '#64748b',
+ textMuted: '#94a3b8',
+ borderSubtle: '#e2e8f0',
+ divider: '#f1f5f9',
+ surface: '#ffffff',
+ surfaceAlt: '#f8fafc'
+};
+const a = (c, suffix) => `${c}${suffix}`;
+const tint = (c) => a(c, '08');
+const soft = (c) => a(c, '18');
+const edge = (c) => a(c, '55');
+const BRAND = '#C01227';
+
+// Ordered status progression used for the stepper. Doormile's real status
+// vocabulary β Assignment_Failed and Cancelled are terminal/off-path states
+// shown separately rather than as stepper nodes.
+const STATUS_STEPS = ['Pending_Pickup', 'Miler_Assigned', 'Pickup_Scheduled', 'At_Customer', 'Picked_Up', 'At_Hub', 'Delivered'];
+const STEP_LABELS = {
+ Pending_Pickup: 'Pending Pickup',
+ Miler_Assigned: 'Miler Assigned',
+ Pickup_Scheduled: 'Pickup Scheduled',
+ At_Customer: 'At Customer',
+ Picked_Up: 'Picked Up',
+ At_Hub: 'At Hub',
+ Delivered: 'Delivered'
+};
+
+const InfoRow = ({ icon: Icon, label, value, color = BRAND }) => (
+
+
+
+
+
+
+ {label}
+
+ {value || 'β'}
+
+
+);
+
+const BookingDetail = () => {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
+ const queryClient = useQueryClient();
+
+ const { data, isLoading, isError } = useQuery({
+ queryKey: ['bookingDetail', id],
+ queryFn: () => getorderdetails(id),
+ enabled: Boolean(id)
+ });
+
+ // Response shape is unconfirmed beyond what /admin/bookings' list rows
+ // return β reads defensively off `data.data` first, falling back to the
+ // top-level response, same pattern used for the login response mismatch.
+ const booking = data?.data || data || {};
+
+ const reassignMutation = useMutation({
+ mutationFn: () => autoAssignBooking(id),
+ onSuccess: () => {
+ OpenToast('Reassignment triggered', 'success', 2000);
+ queryClient.invalidateQueries({ queryKey: ['bookingDetail', id] });
+ },
+ onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
+ });
+
+ const cancelMutation = useMutation({
+ mutationFn: () => cancelOrder(id),
+ onSuccess: () => {
+ OpenToast('Booking cancelled', 'success', 2000);
+ queryClient.invalidateQueries({ queryKey: ['bookingDetail', id] });
+ },
+ onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
+ });
+
+ if (isLoading) return ;
+
+ const status = booking.status || 'Pending_Pickup';
+ const isTerminal = ['cancelled', 'assignment_failed'].includes(String(status).toLowerCase());
+ const activeStepIdx = STATUS_STEPS.indexOf(status);
+ const parcels = booking.bookingparcels || booking.parcels || [];
+ const miler = booking.miler || booking.assignedmiler || null;
+ const agentDecision = booking.agent_decision_id ? booking.agentdecision || booking.agent_decision : null;
+
+ return (
+ <>
+
+ } onClick={() => navigate('/nearle/orders')} sx={{ color: DT.textSecondary, textTransform: 'none', fontWeight: 700 }}>
+ Back to Bookings
+
+
+
+
+
+
+
+
+
+
+ {booking.bookingreference || `Booking #${id}`}
+
+ {booking.createdat ? new Date(booking.createdat).toLocaleString() : 'β'}
+
+
+
+
+
+
+
+ {isError && (
+
+ Could not load full booking details β showing whatever came back.
+
+ )}
+
+ {/* Status timeline */}
+ {!isTerminal && (
+
+
+ {STATUS_STEPS.map((s) => (
+
+
+ {STEP_LABELS[s]}
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+ Customer & Pickup
+
+
+
+
+
+
+
+
+
+
+
+ Delivery
+
+
+
+
+
+
+
+
+
+
+ Assigned Miler
+
+ {booking.assignedmileruserid || miler ? (
+
+
+
+
+
+
+ {miler?.displayname || `Miler #${booking.assignedmileruserid}`}
+
+
+ {miler?.phone && (
+
+ {miler.phone}
+
+ )}
+ {miler?.rating != null && (
+
+
+
+ {miler.rating}
+
+
+ )}
+
+
+
+ ) : (
+
+ No miler assigned yet.
+
+ )}
+
+
+
+
+
+
+ Parcel Details
+
+ {parcels.length === 0 ? (
+
+ No parcel details available.
+
+ ) : (
+
+ {parcels.map((p, i) => (
+
+ {p.description || p.name || `Parcel ${i + 1}`}
+
+ {p.weight ? `${p.weight}kg` : ''}
+
+
+ ))}
+
+ )}
+
+
+
+ {agentDecision && (
+
+
+
+
+
+
+
+ AI Assignment Reasoning
+
+
+
+ {agentDecision.reasoning || agentDecision.reason || JSON.stringify(agentDecision)}
+
+
+
+ )}
+
+
+
+ {!isTerminal && (
+ }
+ disabled={reassignMutation.isLoading}
+ onClick={() => reassignMutation.mutate()}
+ sx={{ bgcolor: '#6366f1', '&:hover': { bgcolor: '#4f46e5' } }}
+ >
+ Reassign Miler
+
+ )}
+ {!isTerminal && (
+ }
+ disabled={cancelMutation.isLoading}
+ onClick={() => cancelMutation.mutate()}
+ >
+ Cancel Booking
+
+ )}
+
+ >
+ );
+};
+
+export default BookingDetail;
diff --git a/src/pages/nearle/dashboard/Dashboard.js b/src/pages/nearle/dashboard/Dashboard.js
new file mode 100644
index 0000000..9a1f00d
--- /dev/null
+++ b/src/pages/nearle/dashboard/Dashboard.js
@@ -0,0 +1,317 @@
+import { useNavigate } from 'react-router-dom';
+import { Avatar, Box, Chip, Grid, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography, Button } from '@mui/material';
+import { useQuery } from '@tanstack/react-query';
+import axios from 'axios';
+import dayjs from 'dayjs';
+import {
+ MdOutlineLocalShipping,
+ MdOutlinePendingActions,
+ MdOutlineCheckCircle,
+ MdTwoWheeler,
+ MdOutlineSmartToy,
+ MdDeploymentUnit,
+ MdArrowForward,
+ MdCircle
+} from 'react-icons/md';
+import PageHeader from 'components/nearle_components/PageHeader';
+import StatCard from 'components/nearle_components/StatCard';
+import { fetchHubs } from 'pages/api/api';
+
+const DT = {
+ radiusCard: 16,
+ textPrimary: '#0f172a',
+ textSecondary: '#64748b',
+ textMuted: '#94a3b8',
+ borderSubtle: '#e2e8f0',
+ divider: '#f1f5f9',
+ surface: '#ffffff',
+ surfaceAlt: '#f8fafc'
+};
+const a = (c, suffix) => `${c}${suffix}`;
+const tint = (c) => a(c, '08');
+const soft = (c) => a(c, '18');
+const edge = (c) => a(c, '55');
+const BRAND = '#C01227';
+
+// Static list β the actual multi-agent pipeline (decide-assignment etc.) has
+// no status endpoint yet, so this is presentational only, per spec.
+const AGENTS = [
+ 'Intake Agent',
+ 'Geocoding Agent',
+ 'Pricing Agent',
+ 'Miler Matching Agent',
+ 'Route Optimisation Agent',
+ 'Notification Agent',
+ 'Fraud Detection Agent',
+ 'Reconciliation Agent'
+];
+
+const fetchBookingsByStatus = async (status, pagesize = 1) => {
+ const res = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status, pagesize } });
+ return res.data;
+};
+
+const Dashboard = () => {
+ const navigate = useNavigate();
+
+ const { data: activeStatusCounts, isLoading: activeLoading } = useQuery({
+ queryKey: ['dashboardActiveCounts'],
+ queryFn: async () => {
+ const [assigned, scheduled, atCustomer] = await Promise.all([
+ fetchBookingsByStatus('Miler_Assigned'),
+ fetchBookingsByStatus('Pickup_Scheduled'),
+ fetchBookingsByStatus('At_Customer')
+ ]);
+ return (assigned.total || 0) + (scheduled.total || 0) + (atCustomer.total || 0);
+ }
+ });
+
+ const { data: pendingCount, isLoading: pendingLoading } = useQuery({
+ queryKey: ['dashboardPendingCount'],
+ queryFn: async () => (await fetchBookingsByStatus('Pending_Pickup')).total || 0
+ });
+
+ const { data: deliveredTodayCount, isLoading: deliveredLoading } = useQuery({
+ queryKey: ['dashboardDeliveredToday'],
+ queryFn: async () => (await fetchBookingsByStatus('Delivered')).total || 0
+ });
+
+ const { data: milers = [], isLoading: milersLoading } = useQuery({
+ queryKey: ['dashboardMilers'],
+ queryFn: async () => {
+ const res = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
+ return res.data?.data || [];
+ }
+ });
+ const availableMilers = milers.filter((m) => m.availabilitystatus === 'Available').length;
+
+ const { data: hubs = [] } = useQuery({ queryKey: ['fetchHubs'], queryFn: fetchHubs });
+
+ const { data: recentBookings = [], isLoading: recentLoading } = useQuery({
+ queryKey: ['dashboardRecentBookings'],
+ queryFn: async () => {
+ const res = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { pagesize: 10 } });
+ return res.data?.data || [];
+ }
+ });
+
+ // No `city` field is confirmed on a booking (only pickupaddress/deliveryaddress
+ // were ever specified) -- derives a best-effort city breakdown from hubs
+ // instead, since hubs do have a confirmed `city` field. Flagged rather than
+ // guessed at a booking-level city grouping that may not exist.
+ const cityRows = Object.values(
+ hubs.reduce((acc, h) => {
+ const city = h.city || 'Unknown';
+ if (!acc[city]) acc[city] = { city, milers: 0, hubs: 0 };
+ acc[city].hubs += 1;
+ acc[city].milers += milers.filter((m) => m.hubid === h.hubid).length;
+ return acc;
+ }, {})
+ );
+
+ const kpis = [
+ { key: 'active', label: 'Active Bookings', color: BRAND, icon: MdOutlineLocalShipping, value: activeStatusCounts ?? 0, loading: activeLoading },
+ { key: 'pending', label: 'Pending Assignment', color: '#f59e0b', icon: MdOutlinePendingActions, value: pendingCount ?? 0, loading: pendingLoading },
+ { key: 'delivered', label: 'Delivered Today', color: '#10b981', icon: MdOutlineCheckCircle, value: deliveredTodayCount ?? 0, loading: deliveredLoading },
+ { key: 'milers', label: 'Milers Available', color: '#0ea5e9', icon: MdTwoWheeler, value: availableMilers, loading: milersLoading }
+ ];
+
+ return (
+ <>
+
+
+
+ {kpis.map((item) => {
+ const Icon = item.icon;
+ return (
+
+ } color={item.color} loading={item.loading} />
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ City Breakdown
+
+
+
+
+
+
+ City
+ Hubs
+ Milers
+ Status
+
+
+
+ {cityRows.length === 0 && (
+
+
+ No hub data yet.
+
+
+ )}
+ {cityRows.map((row) => (
+
+ {row.city}
+ {row.hubs}
+ {row.milers}
+
+ 0 ? 'Active' : 'Idle'}
+ sx={{
+ bgcolor: row.milers > 0 ? tint('#10b981') : tint('#94a3b8'),
+ color: row.milers > 0 ? '#10b981' : '#94a3b8',
+ border: `1px solid ${edge(row.milers > 0 ? '#10b981' : '#94a3b8')}`,
+ fontWeight: 700
+ }}
+ />
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ Recent Bookings
+
+
+ } onClick={() => navigate('/nearle/orders')} sx={{ textTransform: 'none', fontWeight: 700 }}>
+ View all
+
+
+
+
+
+
+ Booking
+ Status
+ Created
+
+
+
+ {recentLoading && (
+
+
+ Loadingβ¦
+
+
+ )}
+ {!recentLoading && recentBookings.length === 0 && (
+
+
+ No bookings yet.
+
+
+ )}
+ {recentBookings.map((b) => (
+ navigate(`/nearle/bookings/${b.bookingid}`)}
+ >
+ {b.bookingreference || `#${b.bookingid}`}
+
+
+
+
+ {b.createdat ? dayjs(b.createdat).format('DD/MM/YYYY hh:mm A') : 'β'}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ Agent System Status
+
+
+
+ {AGENTS.map((agent) => (
+
+
+
+
+
+
+ {agent}
+
+
+
+
+
+ Running
+
+
+
+ ))}
+
+
+
+ navigate('/nearle/hubs')}
+ sx={{
+ mt: 2.5,
+ width: '100%',
+ p: 2,
+ borderRadius: `${DT.radiusCard}px`,
+ border: `1px solid ${edge(BRAND)}`,
+ background: tint(BRAND),
+ cursor: 'pointer',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ textAlign: 'left',
+ font: 'inherit'
+ }}
+ >
+
+
+
+
+
+ Manage Hubs
+
+ {hubs.length} hub{hubs.length === 1 ? '' : 's'} configured
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default Dashboard;
diff --git a/src/pages/nearle/deliveries/deliveries.js b/src/pages/nearle/deliveries/deliveries.js
index e546547..245ca12 100644
--- a/src/pages/nearle/deliveries/deliveries.js
+++ b/src/pages/nearle/deliveries/deliveries.js
@@ -25,8 +25,6 @@ import {
MdCancel,
MdInventory2,
MdHourglassEmpty,
- MdRoute,
- MdSkipNext,
MdTune,
MdMyLocation,
MdOutlineLocalShipping,
@@ -161,30 +159,34 @@ const pillFieldSx = () => ({
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: DT.textMuted }
});
-// Status palette β drives tab pills, row status badges, dialogs.
+// Status palette β drives tab pills, row status badges, dialogs. Keys are
+// Doormile's real booking statuses, lowercased (row.status is compared
+// lowercased everywhere below so exact backend casing doesn't matter).
const STATUS_META = {
- pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
- accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin },
- arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn },
- picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 },
- active: { label: 'Active', color: '#14b8a6', icon: MdRoute },
- skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext },
- delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
- cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
+ all: { label: 'All', color: '#94a3b8', icon: MdAllInclusive },
+ pending_pickup: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
+ miler_assigned: { label: 'Assigned', color: '#6366f1', icon: MdPersonPin },
+ pickup_scheduled: { label: 'Scheduled', color: '#06b6d4', icon: MdHistoryToggleOff },
+ at_customer: { label: 'At Customer', color: '#8b5cf6', icon: MdLocationOn },
+ picked_up: { label: 'Picked Up', color: '#14b8a6', icon: MdInventory2 },
+ at_hub: { label: 'At Hub', color: '#0ea5e9', icon: MdStorefront },
+ delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
+ cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
};
-// Ordered status list driving the tabs row (left β right). Each entry binds
-// the visual meta above to the `currentStatus` key the queries use AND to the
-// `batchCounts` key (so the chip count for the tab is one lookup).
+// Ordered status list driving the tabs row (left β right). `status` is the
+// lowercased key into STATUS_META/batchCounts; `apiStatus` is the exact
+// casing Doormile's /admin/bookings status filter expects.
const STATUS_TABS = [
- { status: 'pending', countKey: 'uncoveredLength' },
- { status: 'accepted', countKey: 'assignedLength' },
- { status: 'arrived', countKey: 'arrivedLength' },
- { status: 'picked', countKey: 'pickedLength' },
- { status: 'active', countKey: 'activeLength' },
- { status: 'skipped', countKey: 'skippedLength' },
- { status: 'delivered', countKey: 'coveredLength' },
- { status: 'cancelled', countKey: 'cancelLength' }
+ { status: 'all', apiStatus: 'all', countKey: 'all' },
+ { status: 'pending_pickup', apiStatus: 'Pending_Pickup', countKey: 'pending_pickup' },
+ { status: 'miler_assigned', apiStatus: 'Miler_Assigned', countKey: 'miler_assigned' },
+ { status: 'pickup_scheduled', apiStatus: 'Pickup_Scheduled', countKey: 'pickup_scheduled' },
+ { status: 'at_customer', apiStatus: 'At_Customer', countKey: 'at_customer' },
+ { status: 'picked_up', apiStatus: 'Picked_Up', countKey: 'picked_up' },
+ { status: 'at_hub', apiStatus: 'At_Hub', countKey: 'at_hub' },
+ { status: 'delivered', apiStatus: 'Delivered', countKey: 'delivered' },
+ { status: 'cancelled', apiStatus: 'Cancelled', countKey: 'cancelled' }
];
// KPI palette + icons β mirrors the four cards across the top of the page.
@@ -321,7 +323,7 @@ const Deliveries = () => {
const [appId, setAppId] = useState(0);
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
- const [tabstatus, setTabstatus] = useState('Pending');
+ const [tabstatus, setTabstatus] = useState('All');
const [tabvalue, setTabvalue] = useState(0);
const [open, setOpen] = useState(false);
const [kms, setKms] = useState('');
@@ -331,7 +333,7 @@ const Deliveries = () => {
const [currentorder, setCurrentorder] = useState({});
const [deliverylat, setDeliverylat] = useState('');
const [deliverylong, setDeliverylong] = useState('');
- const [currentStatus, setCurrentStatus] = useState('pending');
+ const [currentStatus, setCurrentStatus] = useState('all');
const [updateStatus, setUpdateStatus] = useState('delivered');
const locationRef = useRef(null);
const tenantRef = useRef(null);
@@ -516,40 +518,9 @@ const Deliveries = () => {
setPage(0);
setTabvalue(i);
setRowsPerPage(50);
- if (i === 0) {
- setTabstatus('Pending');
- setCurrentStatus('pending');
- }
- if (i === 1) {
- setTabstatus('Assigned');
- setCurrentStatus('accepted');
- }
- if (i === 2) {
- setTabstatus('Arrived');
- setCurrentStatus('arrived');
- }
- if (i === 3) {
- setTabstatus('Picked');
- setCurrentStatus('picked');
- }
- if (i === 4) {
- setTabstatus('Active');
- setCurrentStatus('active');
- }
- if (i === 5) {
- setTabstatus('Skipped');
- setCurrentStatus('skipped');
- }
- if (i === 6) {
- setTabstatus('Delivered');
- setCurrentStatus('delivered');
- }
-
- if (i === 7) {
- setTabstatus('Cancelled');
- setCurrentStatus('cancelled');
- }
- console.log(i);
+ const tab = STATUS_TABS[i];
+ setTabstatus(STATUS_META[tab.status]?.label || tab.status);
+ setCurrentStatus(tab.apiStatus);
setSearchword('');
};
@@ -647,21 +618,10 @@ const Deliveries = () => {
const q = String(debouncedSearch || '').trim().toLowerCase();
return countSourceRows.filter((r) => {
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false;
- const s = String(r.orderstatus || '').toLowerCase();
- if (wantStatus && s !== wantStatus) return false;
+ const s = String(r.status || '').toLowerCase();
+ if (wantStatus && wantStatus !== 'all' && s !== wantStatus) return false;
if (q) {
- const hay = [
- r.deliverycustomer,
- r.deliveryaddress,
- r.deliverysuburb,
- r.pickupcustomer,
- r.pickupaddress,
- r.pickupsuburb,
- r.orderid,
- r.tenantname,
- r.ridername,
- r.username
- ]
+ const hay = [r.bookingreference, r.bookingid, r.pickupaddress, r.deliveryaddress, r.assignedmileruserid]
.map((v) => String(v || '').toLowerCase())
.join(' ');
if (!hay.includes(q)) return false;
@@ -674,49 +634,12 @@ const Deliveries = () => {
// *Length keys returned by fetchCountAPI so the JSX swap-in is mechanical
// (countData?.uncoveredLength β batchCounts.uncoveredLength).
const batchCounts = useMemo(() => {
- const c = {
- uncoveredLength: 0,
- assignedLength: 0,
- arrivedLength: 0,
- pickedLength: 0,
- activeLength: 0,
- skippedLength: 0,
- coveredLength: 0,
- cancelLength: 0
- };
+ const c = {};
countSourceRows.forEach((r) => {
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return;
- const s = String(r.orderstatus || '').toLowerCase();
- switch (s) {
- case 'pending':
- c.uncoveredLength += 1;
- break;
- case 'accepted':
- case 'assigned':
- c.assignedLength += 1;
- break;
- case 'arrived':
- c.arrivedLength += 1;
- break;
- case 'picked':
- c.pickedLength += 1;
- break;
- case 'active':
- c.activeLength += 1;
- break;
- case 'skipped':
- c.skippedLength += 1;
- break;
- case 'delivered':
- c.coveredLength += 1;
- break;
- case 'cancelled':
- case 'canceled':
- c.cancelLength += 1;
- break;
- default:
- break;
- }
+ const s = String(r.status || '').toLowerCase();
+ c[s] = (c[s] || 0) + 1;
+ c.all = (c.all || 0) + 1;
});
return c;
}, [countSourceRows, selectedBatch]);
@@ -1433,7 +1356,7 @@ const Deliveries = () => {
{(() => {
const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered';
const showSelect = tabstatus == 'Created';
- const totalCols = 15 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
+ const totalCols = 7 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
return isMobile ? (
/* ===================== MOBILE: card list ===================== */
@@ -1461,15 +1384,14 @@ const Deliveries = () => {
)}
{filteredRows.map((row, index) => {
- const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || {
- label: row.orderstatus || 'β',
+ const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
+ label: row.status || 'β',
color: '#94a3b8',
icon: MdHistoryToggleOff
};
const RowStatusIcon = rowStatusMeta.icon;
const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
const isOpen = productCollapse?.orderid === row?.orderid;
- const chipSx = (c) => ({ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', color: c, fontWeight: 700, fontSize: 12, whiteSpace: 'nowrap' });
return (
{
- {row.tenantname}
+ {row.bookingreference || `#${row.bookingid}`}
- {[row.tenantsuburb, row.applocation].filter(Boolean).join(' Β· ') || 'β'}
+ {row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : 'β'}
}
>
-
+
- {`${row.locationname}-(${row.locationsuburb})`}
-
-
- {row.orderid} Β· {row.deliveryid}
+ {row.pickupaddress || 'β'}
-
+
- {row.pickupcustomer || 'β'}
-
-
- {row.pickupcontactno}
+ {row.deliveryaddress || 'β'}
-
-
- {row.deliverycustomer || 'β'}
-
-
- {row.deliverycontactno}
-
-
-
- {row.ridername ? (
+
+ {row.assignedmileruserid ? (
-
-
- {row.ridername}
-
-
- ID #{row.userid} Β· {row.ridercontact || 'β'}
-
-
+
+ #{row.assignedmileruserid}
+
) : (
@@ -1606,40 +1509,6 @@ const Deliveries = () => {
)}
-
-
- {row.transitminutes || 0}m
-
-
-
- {row.kms || 0} km
- {row.cumulativekms || 0} km
-
-
-
-
- βΉ {row.deliverycharges?.toFixed(2) ?? '0.00'}
- βΉ {row.deliveryamt?.toFixed(2) ?? '0.00'}
-
-
-
-
-
- {row.collectionamt ? `βΉ ${row.collectionamt.toFixed(2)}` : 'β'}
-
-
-
- {row.step ? (
- {row.step}
- ) : (
- β
- )}
-
- {row.notes && (
-
- {row.notes}
-
- )}
{isOpen && (
@@ -1726,19 +1595,11 @@ const Deliveries = () => {
)}
#
Status
- Tenant
- Order / Location
+ Booking
Pickup
- Drop
- Rider
- ETA
- Transit
- Kms
- Amount
- Notes
- Step
- Qty
- COD
+ Delivery
+ Miler
+ Created
{showAction && Action}
@@ -1774,8 +1635,8 @@ const Deliveries = () => {
)}
{filteredRows.map((row, index) => {
- const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || {
- label: row.orderstatus || 'β',
+ const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
+ label: row.status || 'β',
color: '#94a3b8',
icon: MdHistoryToggleOff
};
@@ -1856,108 +1717,38 @@ const Deliveries = () => {
- {/* Tenants */}
+ {/* Booking */}
-
-
-
- {row.tenantname}
-
-
- {row.tenantsuburb}
-
-
- {row.applocation}
-
-
+
+ {row.bookingreference || `#${row.bookingid}`}
+
+
+ {/* Pickup */}
+
+
+
+ {row.pickupaddress || 'β'}
+
- {/* order details */}
-
-
-
- {`${row.locationname}-(${row.locationsuburb})`}
+ {/* Delivery */}
+
+
+
+ {row.deliveryaddress || 'β'}
-
-
-
-
-
- {row.orderid}
-
-
-
-
- {dayjs(row.orderdate).utc().format('DD/MM/YYYY')}
-
-
- {dayjs(row.orderdate).utc().format('hh:mm A')}
-
-
-
- -
-
-
-
- {row.deliveryid}
-
-
-
-
- {dayjs(row.deliverydate).utc().format('DD/MM/YYYY')}
-
-
-
- {dayjs(row.deliverydate).utc().format('hh:mm A')}
-
-
-
-
- {/* pickup */}
-
-
-
- {row.pickupcustomer}
-
- {row.pickupcontactno}
-
-
- {row.pickuplocation || (row.Pickupaddress ? row.Pickupaddress.slice(0, 14) + 'β¦' : 'β')}
-
-
-
-
- {/* drop */}
-
-
-
- {row.deliverycustomer}
-
- {row.deliverycontactno}
-
-
- {row.deliverylocation || (row.deliveryaddress ? row.deliveryaddress.slice(0, 14) + 'β¦' : 'β')}
-
-
-
-
- {/* rider */}
+ {/* Miler */}
- {row.ridername ? (
+ {row.assignedmileruserid ? (
-
-
- {row.ridername}
-
-
- ID #{row.userid} Β· {row.ridercontact || 'β'}
-
-
+
+ #{row.assignedmileruserid}
+
) : (
@@ -1965,116 +1756,10 @@ const Deliveries = () => {
)}
- {/* Estimated Delivery Time */}
-
-
- {row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : 'β'}
-
-
- {/* Transit Minutes */}
-
-
- {row.transitminutes || 0}m
-
-
- {/* kms */}
+ {/* Created */}
-
-
-
- {row.kms || 0} km
-
-
-
-
- {row.cumulativekms || 0} km
-
-
-
-
- {/* amount */}
-
-
-
-
- βΉ {row.deliverycharges?.toFixed(2) ?? '0.00'}
-
-
-
-
- βΉ {row.deliveryamt?.toFixed(2) ?? '0.00'}
-
-
-
-
- {/* notes */}
-
- {row.notes ? (
-
-
- {row.notes}
-
-
- ) : (
- β
- )}
-
- {/* step */}
-
- {row.step ? (
-
- {row.step}
-
- ) : (
- β
- )}
-
- {/* qty */}
-
-
- {row.Quantity || 'β'}
-
-
- {/* COD */}
-
-
- {row.collectionamt ? `βΉ ${row.collectionamt.toFixed(2)}` : 'β'}
+
+ {row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : 'β'}
{/* Action */}
@@ -2278,7 +1963,7 @@ const Deliveries = () => {
}
}}
>
- {selectedRow?.orderstatus !== 'delivered' && (
+ {String(selectedRow?.status || '').toLowerCase() !== 'delivered' && (
)}
- {['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && (
+ {['pending_pickup', 'miler_assigned', 'pickup_scheduled', 'at_customer'].includes(
+ String(selectedRow?.status || '').toLowerCase()
+ ) && (
)}
- {selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && (
+ {!['cancelled', 'delivered'].includes(String(selectedRow?.status || '').toLowerCase()) && (