feat: Doormile console Phase 2 complete
Part A: remaining api.js legacy functions replaced - fetchRidersList, cancelOrder/Delivery, changeRiderAPI - fetchPercentageAPI, fetchCountAPI → Doormile status counts - getpricinglist, fetchLocations, fetchRidersLogs, getusers - fetchPaymentType, getorderdetails Part B: deliveries.js status tabs → Doormile statuses Part C: 3 new pages - pages/nearle/hubs/Hubs.js (hub CRUD + miler counts) - pages/nearle/bookings/BookingDetail.js (booking detail + timeline) - pages/nearle/dashboard/Dashboard.js (live KPIs + city breakdown) Part D: routes + menu updated
This commit is contained in:
@@ -58,6 +58,13 @@ const nearle = {
|
|||||||
icon: icons.FileDoneOutlined,
|
icon: icons.FileDoneOutlined,
|
||||||
type: 'group',
|
type: 'group',
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
id: 'dashboard',
|
||||||
|
title: <FormattedMessage id="dashboard" />,
|
||||||
|
type: 'item',
|
||||||
|
url: '/nearle/dashboard',
|
||||||
|
icon: icons.DashboardOutlined
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'dispatch',
|
id: 'dispatch',
|
||||||
title: <FormattedMessage id="dispatch" />,
|
title: <FormattedMessage id="dispatch" />,
|
||||||
@@ -107,6 +114,13 @@ const nearle = {
|
|||||||
type: 'item',
|
type: 'item',
|
||||||
url: '/nearle/riders',
|
url: '/nearle/riders',
|
||||||
icon: DirectionsBikeOutlinedIcon
|
icon: DirectionsBikeOutlinedIcon
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hubs',
|
||||||
|
title: <FormattedMessage id="hubs" />,
|
||||||
|
type: 'item',
|
||||||
|
url: '/nearle/hubs',
|
||||||
|
icon: icons.DeploymentUnitOutlined
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,21 @@ export const fetchAppLocations = async () => {
|
|||||||
return [...hubs, { locationname: 'All', applocationid: 0 }];
|
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) ||============================== //
|
// ==============================|| fetchPercentageData (orders) ||============================== //
|
||||||
|
|
||||||
export const fetchPercentageData = async ({ queryKey }) => {
|
export const fetchPercentageData = async ({ queryKey }) => {
|
||||||
@@ -128,33 +143,25 @@ export const fetchOrders = async ({ pageParam = 1, queryKey }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| fetchPaymentType (orders) ||============================== //
|
// ==============================|| fetchPaymentType (orders) ||============================== //
|
||||||
|
// No Doormile payment-types endpoint exists yet — static defaults.
|
||||||
export const fetchPaymentType = async () => {
|
export const fetchPaymentType = async () => [
|
||||||
const { data } = await axios.get(`${process.env.REACT_APP_URL}/utils/getapptypes/?tag=paymentmode`);
|
{ apptypeid: 1, typename: 'Cash', label: 'Cash' },
|
||||||
return data.details.map((val) => ({
|
{ apptypeid: 2, typename: 'Online', label: 'Online' },
|
||||||
...val,
|
{ apptypeid: 3, typename: 'COD', label: 'COD' }
|
||||||
label: val.typename
|
];
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==============================|| fetchRidersList (orders) ||============================== //
|
// ==============================|| fetchRidersList (orders) ||============================== //
|
||||||
|
|
||||||
export const fetchRidersList = async ({ queryKey }) => {
|
export const fetchRidersList = async () => {
|
||||||
try {
|
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/milers`);
|
||||||
const [, appId] = queryKey; // Extract appId from queryKey
|
return (res.data?.data || []).map((m) => ({
|
||||||
const { data } = await axios.get(`${process.env.REACT_APP_URL}/partners/getriders/?applocationid=${appId}`);
|
...m,
|
||||||
console.log('data', data);
|
userid: m.userid,
|
||||||
const response = data?.details
|
label: `${m.displayname} | ${m.phone}`,
|
||||||
? data?.details.map((val) => ({
|
firstname: m.displayname,
|
||||||
...val,
|
lastname: '',
|
||||||
label: `${val.firstname} ${val.lastname} | ${val.contactno}`
|
contactno: m.phone
|
||||||
}))
|
}));
|
||||||
: [];
|
|
||||||
return response;
|
|
||||||
} catch (err) {
|
|
||||||
OpenToast(err.message, 'error', 2000);
|
|
||||||
throw err; // 🔥 REQUIRED
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| Doormile assignment (new) ||============================== //
|
// ==============================|| Doormile assignment (new) ||============================== //
|
||||||
@@ -284,13 +291,8 @@ export const notifyRider = async () => ({ success: true });
|
|||||||
|
|
||||||
// ==============================|| cancelOrder (orders) ||============================== //
|
// ==============================|| cancelOrder (orders) ||============================== //
|
||||||
|
|
||||||
export const cancelOrder = async (orderheaderid) => {
|
export const cancelOrder = async (bookingid) => {
|
||||||
const response = await axios.put(`${process.env.REACT_APP_URL}/orders/updateorder`, {
|
const response = await axios.patch(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}/status`, { status: 'Cancelled' });
|
||||||
orderheaderid: orderheaderid,
|
|
||||||
orderstatus: 'cancelled',
|
|
||||||
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
// ==============================|| cancelMultipleOrder (orders) ||============================== //
|
// ==============================|| cancelMultipleOrder (orders) ||============================== //
|
||||||
@@ -337,81 +339,74 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| fetchPercentageAPI (deliveries) ||============================== //
|
// ==============================|| fetchPercentageAPI (deliveries) ||============================== //
|
||||||
|
// UNVERIFIED: status param + pagesize=1-for-count-only pattern hasn't been
|
||||||
export const fetchPercentageAPI = async (appId) => {
|
// confirmed against the real backend the way bookings/clients/milers were —
|
||||||
const url = `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}`;
|
// implemented per explicit instruction, flag if it 404s/403s like
|
||||||
const response = await axios.get(url);
|
// /admin/customers did.
|
||||||
|
export const fetchPercentageAPI = async () => {
|
||||||
const data = response.data.details;
|
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 {
|
return {
|
||||||
coveredOrders: data.delivered.toString(),
|
coveredOrders: delivered.toString(),
|
||||||
cancelledOrders: data.cancelled.toString(),
|
cancelledOrders: cancelled.toString(),
|
||||||
uncoveredOrders: data.pending.toString(),
|
uncoveredOrders: pending.toString(),
|
||||||
assignedOrders: data.accepted.toString(),
|
assignedOrders: assigned.toString(),
|
||||||
createdOrders: data.created.toString(),
|
createdOrders: total.toString(),
|
||||||
closedOrders: data.delivered.toString(),
|
closedOrders: delivered.toString(),
|
||||||
pickedOrders: data.picked.toString(),
|
pickedOrders: picked.toString(),
|
||||||
percentage1: (Math.round((data.pending / data.total) * 100) || 0).toString(),
|
total: total.toString(),
|
||||||
percentage2: (Math.round((data.accepted / data.total) * 100) || 0).toString(),
|
percentage1: (Math.round((pending / total) * 100) || 0).toString(),
|
||||||
percentage3: (Math.round((data.picked / data.total) * 100) || 0).toString(),
|
percentage2: (Math.round((assigned / total) * 100) || 0).toString(),
|
||||||
percentage4: (Math.round((data.delivered / data.total) * 100) || 0).toString()
|
percentage3: (Math.round((picked / total) * 100) || 0).toString(),
|
||||||
|
percentage4: (Math.round((delivered / total) * 100) || 0).toString()
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| fetchCountAPI (deliveries) ||============================== //
|
// ==============================|| fetchCountAPI (deliveries) ||============================== //
|
||||||
|
|
||||||
export const fetchCountAPI = async (appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) => {
|
export const fetchCountAPI = async () => {
|
||||||
const url =
|
const statuses = ['Pending_Pickup', 'Miler_Assigned', 'Pickup_Scheduled', 'At_Customer', 'Picked_Up', 'At_Hub', 'Delivered', 'Cancelled'];
|
||||||
appId == 0
|
const results = await Promise.all(
|
||||||
? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?appuserid=${userid}&fromdate=${startdate}&todate=${enddate}`
|
statuses.map((s) => axios.get(`${process.env.REACT_APP_URL}/admin/bookings`, { params: { status: s, pagesize: 1 } }))
|
||||||
: `${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 [pending, assigned, scheduled, atcustomer, picked, athub, delivered, cancelled] = results.map((r) => r.data?.total || 0);
|
||||||
const data = response.data.details;
|
|
||||||
return {
|
return {
|
||||||
total: data.total,
|
total: pending + assigned + scheduled + atcustomer + picked + athub + delivered + cancelled,
|
||||||
uncoveredLength: data.pending,
|
uncoveredLength: pending,
|
||||||
assignedLength: data.accepted,
|
assignedLength: assigned,
|
||||||
arrivedLength: data.arrived,
|
arrivedLength: scheduled,
|
||||||
pickedLength: data.picked,
|
pickedLength: picked,
|
||||||
activeLength: data.active,
|
activeLength: atcustomer + athub,
|
||||||
coveredLength: data.delivered,
|
coveredLength: delivered,
|
||||||
cancelLength: data.cancelled,
|
cancelLength: cancelled,
|
||||||
skippedLength: data.skipped
|
skippedLength: 0
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| cancelDeliveryAPI (deliveries) ||============================== //
|
// ==============================|| cancelDeliveryAPI (deliveries) ||============================== //
|
||||||
|
|
||||||
export const cancelDeliveryAPI = async (selectedRow, cancelFeed) => {
|
export const cancelDeliveryAPI = async (selectedRow) => {
|
||||||
const payload = {
|
const response = await axios.patch(`${process.env.REACT_APP_URL}/admin/bookings/${selectedRow.bookingid}/status`, {
|
||||||
deliveryid: selectedRow.deliveryid,
|
status: 'Cancelled'
|
||||||
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);
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
// ==============================|| getorderdetails (deliveries) ||============================== //
|
// ==============================|| getorderdetails (deliveries) ||============================== //
|
||||||
export const getorderdetails = async (orderHeaderid) => {
|
export const getorderdetails = async (bookingid) => {
|
||||||
const response = await axios.get(`${process.env.REACT_APP_URL}/orders/getorderdetails?orderheaderid=${orderHeaderid}`);
|
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/bookings/${bookingid}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| changeRiderAPI (deliveries) ||============================== //
|
// ==============================|| changeRiderAPI (deliveries) ||============================== //
|
||||||
|
|
||||||
export const changeRiderAPI = async (selectedRider, selectedRow) => {
|
export const changeRiderAPI = async (selectedRider, selectedRow) => {
|
||||||
console.log('selectedRider', selectedRider);
|
return axios.post(`${process.env.REACT_APP_URL}/hub/bookings/${selectedRow.bookingid}/assign-miler`, {
|
||||||
console.log('selectedRow', selectedRow);
|
mileruserid: selectedRider.userid
|
||||||
|
|
||||||
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')
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
// ==============================|| updateDeliveryAPI (deliveries) ||============================== //
|
// ==============================|| updateDeliveryAPI (deliveries) ||============================== //
|
||||||
@@ -465,30 +460,18 @@ export const gettenantsummary = async () => {
|
|||||||
};
|
};
|
||||||
// ==============================|| getpricinglist (tenants) ||============================== //
|
// ==============================|| getpricinglist (tenants) ||============================== //
|
||||||
|
|
||||||
export const getpricinglist = async ({ queryKey }) => {
|
export const getpricinglist = async () => {
|
||||||
const [, appId] = queryKey;
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/getpricinglist/?moduleid=6&applocationid=${appId}`);
|
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/pricing`);
|
||||||
return response.data.summary; // return only data, keep it clean
|
return response.data?.data || [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err.response?.data?.message || err.message || 'Something went wrong';
|
OpenToast(err.message, 'error', 2000);
|
||||||
OpenToast(message);
|
return [];
|
||||||
return null; // return null for failure
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// ==============================|| getallpricing (clientPricing) ||============================== //
|
// ==============================|| getallpricing (clientPricing) ||============================== //
|
||||||
|
|
||||||
export const getallpricing = async ({ queryKey }) => {
|
export const getallpricing = getpricinglist;
|
||||||
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 [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==============================|| getcustomersummary (customers) ||============================== //
|
// ==============================|| getcustomersummary (customers) ||============================== //
|
||||||
// No dedicated Doormile summary endpoint has been specified for customers —
|
// No dedicated Doormile summary endpoint has been specified for customers —
|
||||||
@@ -621,35 +604,26 @@ export const fetchorderdetails = async ({ queryKey }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| fetchLocations (orders summary))||============================== //
|
// ==============================|| fetchLocations (orders summary))||============================== //
|
||||||
|
// Not needed as a separate concept in Doormile — returns the hub list instead.
|
||||||
export const fetchLocations = async () => {
|
export const fetchLocations = async () => {
|
||||||
const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getpartners`);
|
const response = await axios.get(`${process.env.REACT_APP_URL}/admin/hubs`);
|
||||||
const updatedLocations = [
|
return [
|
||||||
...response.data.details,
|
...(response.data?.data || []).map((h) => ({ ...h, partnername: h.hubname, partnerid: h.hubid })),
|
||||||
{ partnername: 'All', partnerid: -1 } // Add your new object here
|
{ partnername: 'All', partnerid: -1 }
|
||||||
];
|
];
|
||||||
console.log('fetchLocations', updatedLocations);
|
|
||||||
return updatedLocations;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| fetchRidersLogs (RiderLogs)||============================== //
|
// ==============================|| fetchRidersLogs (RiderLogs)||============================== //
|
||||||
|
// No Doormile equivalent yet (real-time miler location stream lands with EMQX).
|
||||||
export const fetchRidersLogs = async ({ queryKey }) => {
|
export const fetchRidersLogs = async () => [];
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==============================|| getusers (viewProfile)||============================== //
|
// ==============================|| getusers (viewProfile)||============================== //
|
||||||
|
|
||||||
export const getusers = async () => {
|
export const getusers = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(`${process.env.REACT_APP_URL}/users/getusers/?configid=9&userid=${userid}`);
|
const res = await axios.get(`${process.env.REACT_APP_URL}/admin/users`);
|
||||||
return res.data.details;
|
return res.data?.data || [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('getusers', err.message);
|
return [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
308
src/pages/nearle/bookings/BookingDetail.js
Normal file
308
src/pages/nearle/bookings/BookingDetail.js
Normal file
@@ -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 }) => (
|
||||||
|
<Stack direction="row" alignItems="flex-start" spacing={1.5}>
|
||||||
|
<Avatar sx={{ width: 32, height: 32, bgcolor: soft(color), color }}>
|
||||||
|
<Icon size={16} />
|
||||||
|
</Avatar>
|
||||||
|
<Box sx={{ minWidth: 0 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.4 }}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ fontWeight: 600, color: DT.textPrimary, wordBreak: 'break-word' }}>{value || '—'}</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
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 <Loader />;
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ mb: 2 }}>
|
||||||
|
<Button startIcon={<MdArrowBack size={16} />} onClick={() => navigate('/nearle/orders')} sx={{ color: DT.textSecondary, textTransform: 'none', fontWeight: 700 }}>
|
||||||
|
Back to Bookings
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
p: { xs: 2, md: 3 },
|
||||||
|
borderRadius: `${DT.radiusCard}px`,
|
||||||
|
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint('#D35968')} 100%)`,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: DT.borderSubtle,
|
||||||
|
mb: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={2}>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||||
|
<Avatar sx={{ width: 48, height: 48, bgcolor: BRAND, color: '#fff' }}>
|
||||||
|
<MdLocalShipping size={24} />
|
||||||
|
</Avatar>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h3">{booking.bookingreference || `Booking #${id}`}</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
|
||||||
|
{booking.createdat ? new Date(booking.createdat).toLocaleString() : '—'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<Chip
|
||||||
|
label={STEP_LABELS[status] || status}
|
||||||
|
sx={{ fontWeight: 800, bgcolor: isTerminal ? soft('#ef4444') : soft(BRAND), color: isTerminal ? '#ef4444' : BRAND, border: `1px solid ${edge(isTerminal ? '#ef4444' : BRAND)}` }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{isError && (
|
||||||
|
<Paper elevation={0} sx={{ p: 2, mb: 2, borderRadius: 2, border: `1px solid ${edge('#ef4444')}`, bgcolor: tint('#ef4444') }}>
|
||||||
|
<Typography sx={{ color: '#ef4444', fontWeight: 600 }}>Could not load full booking details — showing whatever came back.</Typography>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Status timeline */}
|
||||||
|
{!isTerminal && (
|
||||||
|
<Paper elevation={0} sx={{ p: { xs: 2, md: 3 }, mb: 2, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff' }}>
|
||||||
|
<Stepper activeStep={activeStepIdx} alternativeLabel={!isMobile} orientation={isMobile ? 'vertical' : 'horizontal'}>
|
||||||
|
{STATUS_STEPS.map((s) => (
|
||||||
|
<Step key={s}>
|
||||||
|
<StepLabel
|
||||||
|
sx={{
|
||||||
|
'& .MuiStepIcon-root.Mui-active': { color: BRAND },
|
||||||
|
'& .MuiStepIcon-root.Mui-completed': { color: BRAND }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{STEP_LABELS[s]}
|
||||||
|
</StepLabel>
|
||||||
|
</Step>
|
||||||
|
))}
|
||||||
|
</Stepper>
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Grid container spacing={2.5}>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
|
||||||
|
Customer & Pickup
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<InfoRow icon={MdPerson} label="Customer" value={booking.customername || booking.customer?.name} />
|
||||||
|
<InfoRow icon={MdPhone} label="Phone" value={booking.customerphone || booking.customer?.phone} />
|
||||||
|
<InfoRow icon={MdLocationOn} label="Pickup Address" value={booking.pickupaddress} color="#0ea5e9" />
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
|
||||||
|
Delivery
|
||||||
|
</Typography>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<InfoRow icon={MdLocationOn} label="Delivery Address" value={booking.deliveryaddress} color="#10b981" />
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
|
||||||
|
Assigned Miler
|
||||||
|
</Typography>
|
||||||
|
{booking.assignedmileruserid || miler ? (
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||||
|
<Avatar sx={{ width: 44, height: 44, bgcolor: soft('#8b5cf6'), color: '#8b5cf6' }}>
|
||||||
|
<MdTwoWheeler size={22} />
|
||||||
|
</Avatar>
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||||
|
{miler?.displayname || `Miler #${booking.assignedmileruserid}`}
|
||||||
|
</Typography>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.5} sx={{ mt: 0.25 }}>
|
||||||
|
{miler?.phone && (
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||||
|
{miler.phone}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{miler?.rating != null && (
|
||||||
|
<Stack direction="row" alignItems="center" spacing={0.25}>
|
||||||
|
<MdStar size={13} style={{ color: '#f59e0b' }} />
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||||
|
{miler.rating}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" sx={{ color: DT.textMuted }}>
|
||||||
|
No miler assigned yet.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', height: '100%' }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 2 }}>
|
||||||
|
Parcel Details
|
||||||
|
</Typography>
|
||||||
|
{parcels.length === 0 ? (
|
||||||
|
<Typography variant="body2" sx={{ color: DT.textMuted }}>
|
||||||
|
No parcel details available.
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Stack spacing={1}>
|
||||||
|
{parcels.map((p, i) => (
|
||||||
|
<Stack key={i} direction="row" justifyContent="space-between" sx={{ p: 1, borderRadius: 1.5, bgcolor: DT.surfaceAlt }}>
|
||||||
|
<Typography variant="body2">{p.description || p.name || `Parcel ${i + 1}`}</Typography>
|
||||||
|
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
|
||||||
|
{p.weight ? `${p.weight}kg` : ''}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{agentDecision && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Paper elevation={0} sx={{ p: { xs: 2, md: 2.5 }, borderRadius: `${DT.radiusCard}px`, border: `1px solid ${edge('#6366f1')}`, background: tint('#6366f1') }}>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
|
||||||
|
<Avatar sx={{ width: 32, height: 32, bgcolor: soft('#6366f1'), color: '#6366f1' }}>
|
||||||
|
<MdOutlineSmartToy size={16} />
|
||||||
|
</Avatar>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 800, color: '#6366f1' }}>
|
||||||
|
AI Assignment Reasoning
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Typography variant="body2" sx={{ color: DT.textPrimary }}>
|
||||||
|
{agentDecision.reasoning || agentDecision.reason || JSON.stringify(agentDecision)}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Stack direction="row" spacing={1.5} justifyContent="flex-end" sx={{ mt: 2.5 }}>
|
||||||
|
{!isTerminal && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<MdOutlineAssignmentInd size={16} />}
|
||||||
|
disabled={reassignMutation.isLoading}
|
||||||
|
onClick={() => reassignMutation.mutate()}
|
||||||
|
sx={{ bgcolor: '#6366f1', '&:hover': { bgcolor: '#4f46e5' } }}
|
||||||
|
>
|
||||||
|
Reassign Miler
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!isTerminal && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
startIcon={<MdOutlineCancel size={16} />}
|
||||||
|
disabled={cancelMutation.isLoading}
|
||||||
|
onClick={() => cancelMutation.mutate()}
|
||||||
|
>
|
||||||
|
Cancel Booking
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BookingDetail;
|
||||||
317
src/pages/nearle/dashboard/Dashboard.js
Normal file
317
src/pages/nearle/dashboard/Dashboard.js
Normal file
@@ -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 (
|
||||||
|
<>
|
||||||
|
<PageHeader title="Dashboard" subtitle="Live · Operations overview" live />
|
||||||
|
|
||||||
|
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
|
||||||
|
{kpis.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<Grid item key={item.key} xs={6} sm={3}>
|
||||||
|
<StatCard title={item.label} value={item.value} icon={<Icon size={20} />} color={item.color} loading={item.loading} />
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid container spacing={2.5}>
|
||||||
|
<Grid item xs={12} md={7}>
|
||||||
|
<Paper elevation={0} sx={{ borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', overflow: 'hidden' }}>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1} sx={{ p: 2, borderBottom: `1px solid ${DT.divider}` }}>
|
||||||
|
<MdDeploymentUnit size={18} color={BRAND} />
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
|
||||||
|
City Breakdown
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow sx={{ '& th': { color: DT.textSecondary, fontWeight: 800, fontSize: 11, textTransform: 'uppercase' } }}>
|
||||||
|
<TableCell>City</TableCell>
|
||||||
|
<TableCell align="center">Hubs</TableCell>
|
||||||
|
<TableCell align="center">Milers</TableCell>
|
||||||
|
<TableCell align="center">Status</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{cityRows.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} sx={{ textAlign: 'center', py: 4, color: DT.textMuted }}>
|
||||||
|
No hub data yet.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{cityRows.map((row) => (
|
||||||
|
<TableRow key={row.city} sx={{ '& td': { borderBottom: `1px solid ${DT.divider}` } }}>
|
||||||
|
<TableCell sx={{ fontWeight: 700 }}>{row.city}</TableCell>
|
||||||
|
<TableCell align="center">{row.hubs}</TableCell>
|
||||||
|
<TableCell align="center">{row.milers}</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={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
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper elevation={0} sx={{ mt: 2.5, borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', overflow: 'hidden' }}>
|
||||||
|
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ p: 2, borderBottom: `1px solid ${DT.divider}` }}>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
<MdOutlineLocalShipping size={18} color={BRAND} />
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
|
||||||
|
Recent Bookings
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Button size="small" endIcon={<MdArrowForward size={14} />} onClick={() => navigate('/nearle/orders')} sx={{ textTransform: 'none', fontWeight: 700 }}>
|
||||||
|
View all
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow sx={{ '& th': { color: DT.textSecondary, fontWeight: 800, fontSize: 11, textTransform: 'uppercase' } }}>
|
||||||
|
<TableCell>Booking</TableCell>
|
||||||
|
<TableCell>Status</TableCell>
|
||||||
|
<TableCell>Created</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{recentLoading && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={3} sx={{ textAlign: 'center', py: 4, color: DT.textMuted }}>
|
||||||
|
Loading…
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{!recentLoading && recentBookings.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={3} sx={{ textAlign: 'center', py: 4, color: DT.textMuted }}>
|
||||||
|
No bookings yet.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{recentBookings.map((b) => (
|
||||||
|
<TableRow
|
||||||
|
key={b.bookingid}
|
||||||
|
hover
|
||||||
|
sx={{ cursor: 'pointer', '& td': { borderBottom: `1px solid ${DT.divider}` } }}
|
||||||
|
onClick={() => navigate(`/nearle/bookings/${b.bookingid}`)}
|
||||||
|
>
|
||||||
|
<TableCell sx={{ fontWeight: 700 }}>{b.bookingreference || `#${b.bookingid}`}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip size="small" label={b.status || '—'} sx={{ bgcolor: tint(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}`, fontWeight: 700 }} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell sx={{ color: DT.textSecondary, fontSize: 13 }}>
|
||||||
|
{b.createdat ? dayjs(b.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid item xs={12} md={5}>
|
||||||
|
<Paper elevation={0} sx={{ borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, background: '#fff', overflow: 'hidden' }}>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1} sx={{ p: 2, borderBottom: `1px solid ${DT.divider}` }}>
|
||||||
|
<MdOutlineSmartToy size={18} color="#6366f1" />
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
|
||||||
|
Agent System Status
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing={0} sx={{ p: 1 }}>
|
||||||
|
{AGENTS.map((agent) => (
|
||||||
|
<Stack
|
||||||
|
key={agent}
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
sx={{ px: 1.5, py: 1.25, borderRadius: 2, '&:hover': { bgcolor: DT.surfaceAlt } }}
|
||||||
|
>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.25}>
|
||||||
|
<Avatar sx={{ width: 30, height: 30, bgcolor: soft('#6366f1'), color: '#6366f1' }}>
|
||||||
|
<MdOutlineSmartToy size={15} />
|
||||||
|
</Avatar>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, color: DT.textPrimary }}>
|
||||||
|
{agent}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={0.5}>
|
||||||
|
<MdCircle size={8} color="#10b981" />
|
||||||
|
<Typography variant="caption" sx={{ color: '#10b981', fontWeight: 700 }}>
|
||||||
|
Running
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
component="button"
|
||||||
|
onClick={() => 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'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||||
|
<Avatar sx={{ bgcolor: BRAND, color: '#fff' }}>
|
||||||
|
<MdDeploymentUnit size={18} />
|
||||||
|
</Avatar>
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ fontWeight: 800, color: DT.textPrimary }}>Manage Hubs</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
||||||
|
{hubs.length} hub{hubs.length === 1 ? '' : 's'} configured
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<MdArrowForward size={18} color={BRAND} />
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
@@ -25,8 +25,6 @@ import {
|
|||||||
MdCancel,
|
MdCancel,
|
||||||
MdInventory2,
|
MdInventory2,
|
||||||
MdHourglassEmpty,
|
MdHourglassEmpty,
|
||||||
MdRoute,
|
|
||||||
MdSkipNext,
|
|
||||||
MdTune,
|
MdTune,
|
||||||
MdMyLocation,
|
MdMyLocation,
|
||||||
MdOutlineLocalShipping,
|
MdOutlineLocalShipping,
|
||||||
@@ -161,30 +159,34 @@ const pillFieldSx = () => ({
|
|||||||
'& .MuiAutocomplete-endAdornment .MuiSvgIcon-root': { color: DT.textMuted }
|
'& .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 = {
|
const STATUS_META = {
|
||||||
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
all: { label: 'All', color: '#94a3b8', icon: MdAllInclusive },
|
||||||
accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin },
|
pending_pickup: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||||
arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn },
|
miler_assigned: { label: 'Assigned', color: '#6366f1', icon: MdPersonPin },
|
||||||
picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 },
|
pickup_scheduled: { label: 'Scheduled', color: '#06b6d4', icon: MdHistoryToggleOff },
|
||||||
active: { label: 'Active', color: '#14b8a6', icon: MdRoute },
|
at_customer: { label: 'At Customer', color: '#8b5cf6', icon: MdLocationOn },
|
||||||
skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext },
|
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 },
|
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
||||||
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ordered status list driving the tabs row (left → right). Each entry binds
|
// Ordered status list driving the tabs row (left → right). `status` is the
|
||||||
// the visual meta above to the `currentStatus` key the queries use AND to the
|
// lowercased key into STATUS_META/batchCounts; `apiStatus` is the exact
|
||||||
// `batchCounts` key (so the chip count for the tab is one lookup).
|
// casing Doormile's /admin/bookings status filter expects.
|
||||||
const STATUS_TABS = [
|
const STATUS_TABS = [
|
||||||
{ status: 'pending', countKey: 'uncoveredLength' },
|
{ status: 'all', apiStatus: 'all', countKey: 'all' },
|
||||||
{ status: 'accepted', countKey: 'assignedLength' },
|
{ status: 'pending_pickup', apiStatus: 'Pending_Pickup', countKey: 'pending_pickup' },
|
||||||
{ status: 'arrived', countKey: 'arrivedLength' },
|
{ status: 'miler_assigned', apiStatus: 'Miler_Assigned', countKey: 'miler_assigned' },
|
||||||
{ status: 'picked', countKey: 'pickedLength' },
|
{ status: 'pickup_scheduled', apiStatus: 'Pickup_Scheduled', countKey: 'pickup_scheduled' },
|
||||||
{ status: 'active', countKey: 'activeLength' },
|
{ status: 'at_customer', apiStatus: 'At_Customer', countKey: 'at_customer' },
|
||||||
{ status: 'skipped', countKey: 'skippedLength' },
|
{ status: 'picked_up', apiStatus: 'Picked_Up', countKey: 'picked_up' },
|
||||||
{ status: 'delivered', countKey: 'coveredLength' },
|
{ status: 'at_hub', apiStatus: 'At_Hub', countKey: 'at_hub' },
|
||||||
{ status: 'cancelled', countKey: 'cancelLength' }
|
{ 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.
|
// 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 [appId, setAppId] = useState(0);
|
||||||
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
|
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
|
||||||
const [enddate, setEnddate] = 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 [tabvalue, setTabvalue] = useState(0);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [kms, setKms] = useState('');
|
const [kms, setKms] = useState('');
|
||||||
@@ -331,7 +333,7 @@ const Deliveries = () => {
|
|||||||
const [currentorder, setCurrentorder] = useState({});
|
const [currentorder, setCurrentorder] = useState({});
|
||||||
const [deliverylat, setDeliverylat] = useState('');
|
const [deliverylat, setDeliverylat] = useState('');
|
||||||
const [deliverylong, setDeliverylong] = useState('');
|
const [deliverylong, setDeliverylong] = useState('');
|
||||||
const [currentStatus, setCurrentStatus] = useState('pending');
|
const [currentStatus, setCurrentStatus] = useState('all');
|
||||||
const [updateStatus, setUpdateStatus] = useState('delivered');
|
const [updateStatus, setUpdateStatus] = useState('delivered');
|
||||||
const locationRef = useRef(null);
|
const locationRef = useRef(null);
|
||||||
const tenantRef = useRef(null);
|
const tenantRef = useRef(null);
|
||||||
@@ -516,40 +518,9 @@ const Deliveries = () => {
|
|||||||
setPage(0);
|
setPage(0);
|
||||||
setTabvalue(i);
|
setTabvalue(i);
|
||||||
setRowsPerPage(50);
|
setRowsPerPage(50);
|
||||||
if (i === 0) {
|
const tab = STATUS_TABS[i];
|
||||||
setTabstatus('Pending');
|
setTabstatus(STATUS_META[tab.status]?.label || tab.status);
|
||||||
setCurrentStatus('pending');
|
setCurrentStatus(tab.apiStatus);
|
||||||
}
|
|
||||||
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);
|
|
||||||
setSearchword('');
|
setSearchword('');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -647,21 +618,10 @@ const Deliveries = () => {
|
|||||||
const q = String(debouncedSearch || '').trim().toLowerCase();
|
const q = String(debouncedSearch || '').trim().toLowerCase();
|
||||||
return countSourceRows.filter((r) => {
|
return countSourceRows.filter((r) => {
|
||||||
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false;
|
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return false;
|
||||||
const s = String(r.orderstatus || '').toLowerCase();
|
const s = String(r.status || '').toLowerCase();
|
||||||
if (wantStatus && s !== wantStatus) return false;
|
if (wantStatus && wantStatus !== 'all' && s !== wantStatus) return false;
|
||||||
if (q) {
|
if (q) {
|
||||||
const hay = [
|
const hay = [r.bookingreference, r.bookingid, r.pickupaddress, r.deliveryaddress, r.assignedmileruserid]
|
||||||
r.deliverycustomer,
|
|
||||||
r.deliveryaddress,
|
|
||||||
r.deliverysuburb,
|
|
||||||
r.pickupcustomer,
|
|
||||||
r.pickupaddress,
|
|
||||||
r.pickupsuburb,
|
|
||||||
r.orderid,
|
|
||||||
r.tenantname,
|
|
||||||
r.ridername,
|
|
||||||
r.username
|
|
||||||
]
|
|
||||||
.map((v) => String(v || '').toLowerCase())
|
.map((v) => String(v || '').toLowerCase())
|
||||||
.join(' ');
|
.join(' ');
|
||||||
if (!hay.includes(q)) return false;
|
if (!hay.includes(q)) return false;
|
||||||
@@ -674,49 +634,12 @@ const Deliveries = () => {
|
|||||||
// *Length keys returned by fetchCountAPI so the JSX swap-in is mechanical
|
// *Length keys returned by fetchCountAPI so the JSX swap-in is mechanical
|
||||||
// (countData?.uncoveredLength → batchCounts.uncoveredLength).
|
// (countData?.uncoveredLength → batchCounts.uncoveredLength).
|
||||||
const batchCounts = useMemo(() => {
|
const batchCounts = useMemo(() => {
|
||||||
const c = {
|
const c = {};
|
||||||
uncoveredLength: 0,
|
|
||||||
assignedLength: 0,
|
|
||||||
arrivedLength: 0,
|
|
||||||
pickedLength: 0,
|
|
||||||
activeLength: 0,
|
|
||||||
skippedLength: 0,
|
|
||||||
coveredLength: 0,
|
|
||||||
cancelLength: 0
|
|
||||||
};
|
|
||||||
countSourceRows.forEach((r) => {
|
countSourceRows.forEach((r) => {
|
||||||
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return;
|
if (selectedBatch !== 'all' && getRowBatchId(r) !== selectedBatch) return;
|
||||||
const s = String(r.orderstatus || '').toLowerCase();
|
const s = String(r.status || '').toLowerCase();
|
||||||
switch (s) {
|
c[s] = (c[s] || 0) + 1;
|
||||||
case 'pending':
|
c.all = (c.all || 0) + 1;
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return c;
|
return c;
|
||||||
}, [countSourceRows, selectedBatch]);
|
}, [countSourceRows, selectedBatch]);
|
||||||
@@ -1433,7 +1356,7 @@ const Deliveries = () => {
|
|||||||
{(() => {
|
{(() => {
|
||||||
const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered';
|
const showAction = tabstatus !== 'Cancelled' && tabstatus !== 'Delivered';
|
||||||
const showSelect = tabstatus == 'Created';
|
const showSelect = tabstatus == 'Created';
|
||||||
const totalCols = 15 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
|
const totalCols = 7 + (showAction ? 1 : 0) + (showSelect ? 1 : 0);
|
||||||
return isMobile ? (
|
return isMobile ? (
|
||||||
/* ===================== MOBILE: card list ===================== */
|
/* ===================== MOBILE: card list ===================== */
|
||||||
<MobileCardList sx={{ p: 1.25 }}>
|
<MobileCardList sx={{ p: 1.25 }}>
|
||||||
@@ -1461,15 +1384,14 @@ const Deliveries = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
{filteredRows.map((row, index) => {
|
{filteredRows.map((row, index) => {
|
||||||
const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || {
|
const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
|
||||||
label: row.orderstatus || '—',
|
label: row.status || '—',
|
||||||
color: '#94a3b8',
|
color: '#94a3b8',
|
||||||
icon: MdHistoryToggleOff
|
icon: MdHistoryToggleOff
|
||||||
};
|
};
|
||||||
const RowStatusIcon = rowStatusMeta.icon;
|
const RowStatusIcon = rowStatusMeta.icon;
|
||||||
const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
|
const isSelected = !!deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid);
|
||||||
const isOpen = productCollapse?.orderid === row?.orderid;
|
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 (
|
return (
|
||||||
<MobileCard
|
<MobileCard
|
||||||
key={row.orderheaderid ?? `${row.tenantname}-${index}`}
|
key={row.orderheaderid ?? `${row.tenantname}-${index}`}
|
||||||
@@ -1551,54 +1473,35 @@ const Deliveries = () => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
<Box sx={{ minWidth: 0 }}>
|
<Box sx={{ minWidth: 0 }}>
|
||||||
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
|
<Typography sx={{ fontWeight: 800, color: DT.textPrimary, fontSize: 15 }} noWrap>
|
||||||
{row.tenantname}
|
{row.bookingreference || `#${row.bookingid}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
||||||
{[row.tenantsuburb, row.applocation].filter(Boolean).join(' · ') || '—'}
|
{row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MobileFieldGrid>
|
<MobileFieldGrid>
|
||||||
<MobileField label="Order / Location" full>
|
<MobileField label="Pickup" full>
|
||||||
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
|
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
|
||||||
{`${row.locationname}-(${row.locationsuburb})`}
|
{row.pickupaddress || '—'}
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
|
||||||
{row.orderid} · {row.deliveryid}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</MobileField>
|
</MobileField>
|
||||||
<MobileField label="Pickup">
|
<MobileField label="Delivery" full>
|
||||||
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
|
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
|
||||||
{row.pickupcustomer || '—'}
|
{row.deliveryaddress || '—'}
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
|
||||||
{row.pickupcontactno}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</MobileField>
|
</MobileField>
|
||||||
<MobileField label="Drop">
|
<MobileField label="Miler" full>
|
||||||
<Typography sx={{ fontSize: 13, fontWeight: 600, color: DT.textPrimary }} noWrap>
|
{row.assignedmileruserid ? (
|
||||||
{row.deliverycustomer || '—'}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
|
||||||
{row.deliverycontactno}
|
|
||||||
</Typography>
|
|
||||||
</MobileField>
|
|
||||||
<MobileField label="Rider" full>
|
|
||||||
{row.ridername ? (
|
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
<AccentAvatar color="#8b5cf6" size={24}>
|
<AccentAvatar color="#8b5cf6" size={24}>
|
||||||
<MdDirectionsBike size={13} />
|
<MdDirectionsBike size={13} />
|
||||||
</AccentAvatar>
|
</AccentAvatar>
|
||||||
<Stack sx={{ minWidth: 0 }}>
|
|
||||||
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
|
<Typography sx={{ fontSize: 13, fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||||
{row.ridername}
|
#{row.assignedmileruserid}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
|
||||||
ID #{row.userid} · {row.ridercontact || '—'}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
|
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
|
||||||
@@ -1606,40 +1509,6 @@ const Deliveries = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</MobileField>
|
</MobileField>
|
||||||
<MobileField label="ETA" value={row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'} />
|
|
||||||
<MobileField label="Transit">
|
|
||||||
<Box sx={chipSx('#06b6d4')}>{row.transitminutes || 0}m</Box>
|
|
||||||
</MobileField>
|
|
||||||
<MobileField label="Kms · plan / act">
|
|
||||||
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
|
|
||||||
<Box sx={chipSx('#ef4444')}>{row.kms || 0} km</Box>
|
|
||||||
<Box sx={chipSx('#10b981')}>{row.cumulativekms || 0} km</Box>
|
|
||||||
</Stack>
|
|
||||||
</MobileField>
|
|
||||||
<MobileField label="Amount · chg / amt">
|
|
||||||
<Stack direction="row" spacing={0.5} flexWrap="wrap" useFlexGap>
|
|
||||||
<Box sx={chipSx('#ef4444')}>₹ {row.deliverycharges?.toFixed(2) ?? '0.00'}</Box>
|
|
||||||
<Box sx={chipSx('#10b981')}>₹ {row.deliveryamt?.toFixed(2) ?? '0.00'}</Box>
|
|
||||||
</Stack>
|
|
||||||
</MobileField>
|
|
||||||
<MobileField label="Qty" value={row.Quantity || '—'} />
|
|
||||||
<MobileField label="COD">
|
|
||||||
<Typography sx={{ fontSize: 13, fontWeight: 800, color: row.collectionamt ? '#ef4444' : DT.textMuted }}>
|
|
||||||
{row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'}
|
|
||||||
</Typography>
|
|
||||||
</MobileField>
|
|
||||||
<MobileField label="Step">
|
|
||||||
{row.step ? (
|
|
||||||
<Box sx={{ ...chipSx('#C01227'), minWidth: 30, fontWeight: 800 }}>{row.step}</Box>
|
|
||||||
) : (
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>—</Typography>
|
|
||||||
)}
|
|
||||||
</MobileField>
|
|
||||||
{row.notes && (
|
|
||||||
<MobileField label="Notes" full>
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.notes}</Typography>
|
|
||||||
</MobileField>
|
|
||||||
)}
|
|
||||||
</MobileFieldGrid>
|
</MobileFieldGrid>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<Box sx={{ mt: 1.5, p: 1.25, borderRadius: 2, bgcolor: DT.surfaceAlt, border: `1px solid ${DT.divider}` }}>
|
<Box sx={{ mt: 1.5, p: 1.25, borderRadius: 2, bgcolor: DT.surfaceAlt, border: `1px solid ${DT.divider}` }}>
|
||||||
@@ -1726,19 +1595,11 @@ const Deliveries = () => {
|
|||||||
)}
|
)}
|
||||||
<TableCell>#</TableCell>
|
<TableCell>#</TableCell>
|
||||||
<TableCell>Status</TableCell>
|
<TableCell>Status</TableCell>
|
||||||
<TableCell>Tenant</TableCell>
|
<TableCell>Booking</TableCell>
|
||||||
<TableCell>Order / Location</TableCell>
|
|
||||||
<TableCell>Pickup</TableCell>
|
<TableCell>Pickup</TableCell>
|
||||||
<TableCell>Drop</TableCell>
|
<TableCell>Delivery</TableCell>
|
||||||
<TableCell>Rider</TableCell>
|
<TableCell>Miler</TableCell>
|
||||||
<TableCell>ETA</TableCell>
|
<TableCell>Created</TableCell>
|
||||||
<TableCell>Transit</TableCell>
|
|
||||||
<TableCell>Kms</TableCell>
|
|
||||||
<TableCell>Amount</TableCell>
|
|
||||||
<TableCell>Notes</TableCell>
|
|
||||||
<TableCell>Step</TableCell>
|
|
||||||
<TableCell>Qty</TableCell>
|
|
||||||
<TableCell>COD</TableCell>
|
|
||||||
{showAction && <TableCell align="right">Action</TableCell>}
|
{showAction && <TableCell align="right">Action</TableCell>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
@@ -1774,8 +1635,8 @@ const Deliveries = () => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
{filteredRows.map((row, index) => {
|
{filteredRows.map((row, index) => {
|
||||||
const rowStatusMeta = STATUS_META[String(row.orderstatus || '').toLowerCase()] || {
|
const rowStatusMeta = STATUS_META[String(row.status || '').toLowerCase()] || {
|
||||||
label: row.orderstatus || '—',
|
label: row.status || '—',
|
||||||
color: '#94a3b8',
|
color: '#94a3b8',
|
||||||
icon: MdHistoryToggleOff
|
icon: MdHistoryToggleOff
|
||||||
};
|
};
|
||||||
@@ -1856,108 +1717,38 @@ const Deliveries = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{/* Tenants */}
|
{/* Booking */}
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Tooltip title={row.tenantadress}>
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||||
<Stack>
|
{row.bookingreference || `#${row.bookingid}`}
|
||||||
<Typography noWrap variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
|
||||||
{row.tenantname}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography noWrap variant="caption" sx={{ color: DT.textSecondary }}>
|
|
||||||
{row.tenantsuburb}
|
|
||||||
</Typography>
|
|
||||||
<Typography noWrap variant="caption" sx={{ color: DT.textMuted }}>
|
|
||||||
{row.applocation}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Tooltip>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{/* order details */}
|
{/* Pickup */}
|
||||||
<TableCell align="left">
|
<TableCell sx={{ maxWidth: 220 }}>
|
||||||
<Tooltip title="Location Name-Suburb" placement="top">
|
<Tooltip title={row.pickupaddress || ''} placement="top">
|
||||||
<Typography variant="subtitle1" noWrap>
|
|
||||||
{`${row.locationname}-(${row.locationsuburb})`}
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Stack display={'flex'} flexDirection={'row'} gap={3}>
|
|
||||||
<Stack>
|
|
||||||
<Tooltip title="Order Id" placement="top">
|
|
||||||
<Typography variant="body2" noWrap>
|
<Typography variant="body2" noWrap>
|
||||||
{row.orderid}
|
{row.pickupaddress || '—'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Ordered date" placement="top">
|
</TableCell>
|
||||||
<Typography noWrap sx={{ fontSize: '12px' }}>
|
{/* Delivery */}
|
||||||
{dayjs(row.orderdate).utc().format('DD/MM/YYYY')}
|
<TableCell sx={{ maxWidth: 220 }}>
|
||||||
</Typography>
|
<Tooltip title={row.deliveryaddress || ''} placement="top">
|
||||||
<Typography noWrap sx={{ fontSize: '11px' }}>
|
|
||||||
{dayjs(row.orderdate).utc().format('hh:mm A')}
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
-
|
|
||||||
<Stack>
|
|
||||||
<Tooltip title="Delivery Id" placement="top">
|
|
||||||
<Typography variant="body2" noWrap>
|
<Typography variant="body2" noWrap>
|
||||||
{row.deliveryid}
|
{row.deliveryaddress || '—'}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Delivery date" placement="top">
|
|
||||||
<Typography noWrap sx={{ fontSize: '12px' }}>
|
|
||||||
{dayjs(row.deliverydate).utc().format('DD/MM/YYYY')}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography noWrap sx={{ fontSize: '11px' }}>
|
|
||||||
{dayjs(row.deliverydate).utc().format('hh:mm A')}
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{/* pickup */}
|
{/* Miler */}
|
||||||
<TableCell align="left">
|
|
||||||
<Stack>
|
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
|
||||||
{row.pickupcustomer}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.pickupcontactno}</Typography>
|
|
||||||
<Tooltip title={row.Pickupaddress} sx={{ whiteSpace: 'nowrap' }}>
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>
|
|
||||||
{row.pickuplocation || (row.Pickupaddress ? row.Pickupaddress.slice(0, 14) + '…' : '—')}
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
</TableCell>
|
|
||||||
{/* drop */}
|
|
||||||
<TableCell align="left">
|
|
||||||
<Stack>
|
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
|
||||||
{row.deliverycustomer}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>{row.deliverycontactno}</Typography>
|
|
||||||
<Tooltip title={row.deliveryaddress}>
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textMuted, whiteSpace: 'nowrap' }}>
|
|
||||||
{row.deliverylocation || (row.deliveryaddress ? row.deliveryaddress.slice(0, 14) + '…' : '—')}
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
</TableCell>
|
|
||||||
{/* rider */}
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{row.ridername ? (
|
{row.assignedmileruserid ? (
|
||||||
<Stack direction="row" alignItems="center" spacing={1}>
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
<AccentAvatar color="#8b5cf6" size={28}>
|
<AccentAvatar color="#8b5cf6" size={28}>
|
||||||
<MdDirectionsBike size={14} />
|
<MdDirectionsBike size={14} />
|
||||||
</AccentAvatar>
|
</AccentAvatar>
|
||||||
<Stack>
|
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
||||||
{row.ridername}
|
#{row.assignedmileruserid}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
|
||||||
ID #{row.userid} · {row.ridercontact || '—'}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
|
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 600 }}>
|
||||||
@@ -1965,116 +1756,10 @@ const Deliveries = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{/* Estimated Delivery Time */}
|
{/* Created */}
|
||||||
<TableCell align="left">
|
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary, whiteSpace: 'nowrap' }}>
|
|
||||||
{row.expecteddeliverytime ? dayjs(row.expecteddeliverytime).format('hh:mm A') : '—'}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
{/* Transit Minutes */}
|
|
||||||
<TableCell align="left">
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 0.5,
|
|
||||||
px: 1,
|
|
||||||
py: 0.5,
|
|
||||||
borderRadius: 999,
|
|
||||||
bgcolor: tint('#06b6d4'),
|
|
||||||
color: '#06b6d4',
|
|
||||||
fontWeight: 700,
|
|
||||||
fontSize: 12,
|
|
||||||
border: `1px solid ${edge('#06b6d4')}`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{row.transitminutes || 0}m
|
|
||||||
</Box>
|
|
||||||
</TableCell>
|
|
||||||
{/* kms */}
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Stack direction="column" spacing={0.5} alignItems="flex-start">
|
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
||||||
<Tooltip title="Planned KMS" placement="top">
|
{row.createdat ? dayjs(row.createdat).format('DD/MM/YYYY hh:mm A') : '—'}
|
||||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#ef4444'), color: '#ef4444', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#ef4444')}`, whiteSpace: 'nowrap', minWidth: 75 }}>
|
|
||||||
{row.kms || 0} km
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Actual KMS" placement="top">
|
|
||||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#10b981'), color: '#10b981', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#10b981')}`, whiteSpace: 'nowrap', minWidth: 75 }}>
|
|
||||||
{row.cumulativekms || 0} km
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
</TableCell>
|
|
||||||
{/* amount */}
|
|
||||||
<TableCell align="left">
|
|
||||||
<Stack direction="column" spacing={0.5} alignItems="flex-start">
|
|
||||||
<Tooltip title="Delivery Charge" placement="top">
|
|
||||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#ef4444'), color: '#ef4444', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#ef4444')}`, whiteSpace: 'nowrap', minWidth: 85 }}>
|
|
||||||
₹ {row.deliverycharges?.toFixed(2) ?? '0.00'}
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Delivery Amount" placement="top">
|
|
||||||
<Box sx={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', px: 1.25, py: 0.375, borderRadius: 999, bgcolor: tint('#10b981'), color: '#10b981', fontWeight: 700, fontSize: 11, border: `1px solid ${edge('#10b981')}`, whiteSpace: 'nowrap', minWidth: 85 }}>
|
|
||||||
₹ {row.deliveryamt?.toFixed(2) ?? '0.00'}
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
</TableCell>
|
|
||||||
{/* notes */}
|
|
||||||
<TableCell>
|
|
||||||
{row.notes ? (
|
|
||||||
<Tooltip title={row.notes}>
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textSecondary, maxWidth: 160, display: 'inline-block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
||||||
{row.notes}
|
|
||||||
</Typography>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>—</Typography>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
{/* step */}
|
|
||||||
<TableCell align="center">
|
|
||||||
{row.step ? (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
minWidth: 30,
|
|
||||||
height: 24,
|
|
||||||
px: 0.875,
|
|
||||||
borderRadius: 999,
|
|
||||||
bgcolor: tint('#C01227'),
|
|
||||||
border: `1px solid ${edge('#C01227')}`,
|
|
||||||
color: '#C01227',
|
|
||||||
fontWeight: 800,
|
|
||||||
fontSize: 11
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{row.step}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<Typography variant="caption" sx={{ color: DT.textMuted }}>—</Typography>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
{/* qty */}
|
|
||||||
<TableCell>
|
|
||||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: row.Quantity ? DT.textPrimary : DT.textMuted, whiteSpace: 'nowrap' }}>
|
|
||||||
{row.Quantity || '—'}
|
|
||||||
</Typography>
|
|
||||||
</TableCell>
|
|
||||||
{/* COD */}
|
|
||||||
<TableCell>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle2"
|
|
||||||
sx={{
|
|
||||||
fontWeight: 800,
|
|
||||||
color: row.collectionamt ? '#ef4444' : DT.textMuted,
|
|
||||||
whiteSpace: 'nowrap'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{row.collectionamt ? `₹ ${row.collectionamt.toFixed(2)}` : '—'}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{/* Action */}
|
{/* Action */}
|
||||||
@@ -2278,7 +1963,7 @@ const Deliveries = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedRow?.orderstatus !== 'delivered' && (
|
{String(selectedRow?.status || '').toLowerCase() !== 'delivered' && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
notifyRiderMutation.mutate(selectedRow.userfcmtoken);
|
notifyRiderMutation.mutate(selectedRow.userfcmtoken);
|
||||||
@@ -2289,7 +1974,9 @@ const Deliveries = () => {
|
|||||||
Notify Rider
|
Notify Rider
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && (
|
{['pending_pickup', 'miler_assigned', 'pickup_scheduled', 'at_customer'].includes(
|
||||||
|
String(selectedRow?.status || '').toLowerCase()
|
||||||
|
) && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!appId) {
|
if (!appId) {
|
||||||
@@ -2314,7 +2001,7 @@ const Deliveries = () => {
|
|||||||
setDeliverylong(selectedRow.droplon);
|
setDeliverylong(selectedRow.droplon);
|
||||||
setNotes(selectedRow.notes);
|
setNotes(selectedRow.notes);
|
||||||
setDeliveryamount(selectedRow.deliveryamount);
|
setDeliveryamount(selectedRow.deliveryamount);
|
||||||
setUpdateStatus(selectedRow.orderstatus || 'delivered');
|
setUpdateStatus(selectedRow.status || 'delivered');
|
||||||
setCurrentorder(selectedRow);
|
setCurrentorder(selectedRow);
|
||||||
setDialogopen(true);
|
setDialogopen(true);
|
||||||
handleMenuClose();
|
handleMenuClose();
|
||||||
@@ -2324,7 +2011,7 @@ const Deliveries = () => {
|
|||||||
Update Status
|
Update Status
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{selectedRow?.orderstatus !== 'cancelled' && selectedRow?.orderstatus !== 'delivered' && (
|
{!['cancelled', 'delivered'].includes(String(selectedRow?.status || '').toLowerCase()) && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
sx={{ color: '#ef4444 !important' }}
|
sx={{ color: '#ef4444 !important' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
485
src/pages/nearle/hubs/Hubs.js
Normal file
485
src/pages/nearle/hubs/Hubs.js
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
import { useState, Fragment } from 'react';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
Collapse,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
MenuItem,
|
||||||
|
Paper,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TextField,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useMediaQuery
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
MdDeploymentUnit,
|
||||||
|
MdOutlineWarehouse,
|
||||||
|
MdOutlineCheckCircle,
|
||||||
|
MdOutlineBarChart,
|
||||||
|
MdOutlineLocationCity,
|
||||||
|
MdAdd,
|
||||||
|
MdEdit,
|
||||||
|
MdKeyboardArrowDown,
|
||||||
|
MdKeyboardArrowUp,
|
||||||
|
MdTwoWheeler
|
||||||
|
} from 'react-icons/md';
|
||||||
|
import StatCard from 'components/nearle_components/StatCard';
|
||||||
|
import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton';
|
||||||
|
import { OpenToast } from 'components/third-party/OpenToast';
|
||||||
|
import { fetchHubs, createHub, updateHub, fetchAllRiders } from 'pages/api/api';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Design tokens — mirrors the DT block used across the console (see
|
||||||
|
// CLAUDE.md §6). Brand red #C01227 is the canonical primary.
|
||||||
|
// ============================================================================
|
||||||
|
const DT = {
|
||||||
|
radiusPill: 999,
|
||||||
|
radiusCard: 16,
|
||||||
|
radiusInner: 12,
|
||||||
|
shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
|
||||||
|
shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
|
||||||
|
shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
|
||||||
|
textPrimary: '#0f172a',
|
||||||
|
textSecondary: '#64748b',
|
||||||
|
textMuted: '#94a3b8',
|
||||||
|
borderSubtle: '#e2e8f0',
|
||||||
|
divider: '#f1f5f9',
|
||||||
|
surface: '#ffffff',
|
||||||
|
surfaceAlt: '#f8fafc'
|
||||||
|
};
|
||||||
|
|
||||||
|
const 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';
|
||||||
|
|
||||||
|
const CITIES = ['Coimbatore', 'Hyderabad', 'Bangalore', 'Chennai'];
|
||||||
|
const HUB_TYPES = [
|
||||||
|
{ value: 'sorting_center', label: 'Sorting Center' },
|
||||||
|
{ value: 'spoke', label: 'Spoke' },
|
||||||
|
{ value: 'pickup_point', label: 'Pickup Point' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const AccentAvatar = ({ color, selected, size = 24, children }) => (
|
||||||
|
<Avatar
|
||||||
|
sx={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
bgcolor: selected ? color : soft(color),
|
||||||
|
color: selected ? '#fff' : color,
|
||||||
|
transition: 'background-color 0.15s, color 0.15s'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
|
||||||
|
const EMPTY_FORM = { hubname: '', hubtype: 'sorting_center', city: CITIES[0], capacity: '', contact: '', address: '', pincode: '' };
|
||||||
|
|
||||||
|
const Hubs = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [cityFilter, setCityFilter] = useState('All');
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [editingHub, setEditingHub] = useState(null);
|
||||||
|
const [form, setForm] = useState(EMPTY_FORM);
|
||||||
|
const [expandedHubId, setExpandedHubId] = useState(null);
|
||||||
|
|
||||||
|
const { data: hubs = [], isLoading: hubsLoading } = useQuery({
|
||||||
|
queryKey: ['fetchHubs'],
|
||||||
|
queryFn: fetchHubs
|
||||||
|
});
|
||||||
|
|
||||||
|
// Milers filtered per-hub for the expandable row. /admin/milers has no
|
||||||
|
// server-side hub filter (same limitation noted in api.js for fetchAllRiders),
|
||||||
|
// so this fetches the full list once and filters client-side.
|
||||||
|
const { data: allMilersRes } = useQuery({
|
||||||
|
queryKey: ['fetchAllRiders', 0, '', 0],
|
||||||
|
queryFn: fetchAllRiders
|
||||||
|
});
|
||||||
|
const allMilers = allMilersRes?.details || [];
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: createHub,
|
||||||
|
onSuccess: () => {
|
||||||
|
OpenToast('Hub created', 'success', 2000);
|
||||||
|
setDialogOpen(false);
|
||||||
|
setForm(EMPTY_FORM);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['fetchHubs'] });
|
||||||
|
},
|
||||||
|
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: ({ id, body }) => updateHub(id, body),
|
||||||
|
onSuccess: () => {
|
||||||
|
OpenToast('Hub updated', 'success', 2000);
|
||||||
|
setDialogOpen(false);
|
||||||
|
setEditingHub(null);
|
||||||
|
setForm(EMPTY_FORM);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['fetchHubs'] });
|
||||||
|
},
|
||||||
|
onError: (err) => OpenToast(err.response?.data?.message || err.message, 'error', 3000)
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredHubs = cityFilter === 'All' ? hubs : hubs.filter((h) => h.city === cityFilter);
|
||||||
|
|
||||||
|
const kpis = [
|
||||||
|
{ key: 'total', label: 'Total Hubs', color: BRAND, icon: MdOutlineWarehouse, value: hubs.length },
|
||||||
|
{ key: 'active', label: 'Active', color: '#10b981', icon: MdOutlineCheckCircle, value: hubs.filter((h) => (h.status || 'active').toLowerCase() === 'active').length },
|
||||||
|
{
|
||||||
|
key: 'capacity',
|
||||||
|
label: 'Capacity Used',
|
||||||
|
color: '#f59e0b',
|
||||||
|
icon: MdOutlineBarChart,
|
||||||
|
value: hubs.length
|
||||||
|
? `${Math.round((hubs.reduce((s, h) => s + (h.usedcapacity || 0), 0) / (hubs.reduce((s, h) => s + (h.capacity || 0), 0) || 1)) * 100)}%`
|
||||||
|
: '0%'
|
||||||
|
},
|
||||||
|
{ key: 'cities', label: 'Cities', color: '#0ea5e9', icon: MdOutlineLocationCity, value: new Set(hubs.map((h) => h.city).filter(Boolean)).size }
|
||||||
|
];
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
setEditingHub(null);
|
||||||
|
setForm(EMPTY_FORM);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
const openEdit = (hub) => {
|
||||||
|
setEditingHub(hub);
|
||||||
|
setForm({
|
||||||
|
hubname: hub.hubname || '',
|
||||||
|
hubtype: hub.hubtype || 'sorting_center',
|
||||||
|
city: hub.city || CITIES[0],
|
||||||
|
capacity: hub.capacity || '',
|
||||||
|
contact: hub.contact || '',
|
||||||
|
address: hub.address || '',
|
||||||
|
pincode: hub.pincode || ''
|
||||||
|
});
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!form.hubname) {
|
||||||
|
OpenToast('Enter a hub name', 'warning', 2000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (editingHub) {
|
||||||
|
updateMutation.mutate({ id: editingHub.hubid, body: form });
|
||||||
|
} else {
|
||||||
|
createMutation.mutate(form);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* ============================================= || Header || ============================================= */}
|
||||||
|
<Paper
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
p: { xs: 2, md: 3 },
|
||||||
|
borderRadius: `${DT.radiusCard}px`,
|
||||||
|
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint('#D35968')} 100%)`,
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: DT.borderSubtle,
|
||||||
|
mb: { xs: 1.5, md: 2 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction={{ xs: 'column', sm: 'row' }} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }} spacing={2}>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1.5}>
|
||||||
|
<Avatar sx={{ width: 48, height: 48, bgcolor: BRAND, color: '#fff' }}>
|
||||||
|
<MdDeploymentUnit size={24} />
|
||||||
|
</Avatar>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h3">Hubs</Typography>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ mt: 0.25 }}>
|
||||||
|
<Box sx={{ width: 8, height: 8, borderRadius: '50%', bgcolor: '#10b981', animation: 'pulse 1.6s infinite' }} />
|
||||||
|
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
|
||||||
|
Live · {cityFilter}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" spacing={1} flexWrap="wrap">
|
||||||
|
{['All', ...CITIES].map((c) => (
|
||||||
|
<Chip
|
||||||
|
key={c}
|
||||||
|
label={c}
|
||||||
|
onClick={() => setCityFilter(c)}
|
||||||
|
sx={{
|
||||||
|
fontWeight: 700,
|
||||||
|
bgcolor: cityFilter === c ? BRAND : '#fff',
|
||||||
|
color: cityFilter === c ? '#fff' : DT.textSecondary,
|
||||||
|
border: `1px solid ${cityFilter === c ? BRAND : DT.borderSubtle}`,
|
||||||
|
'&:hover': { bgcolor: cityFilter === c ? BRAND : DT.surfaceAlt }
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* ============================================= || KPI Cards || ============================================= */}
|
||||||
|
<Grid container spacing={{ xs: 2, md: 2.5 }} sx={{ mb: { xs: 1.5, md: 2 } }}>
|
||||||
|
{kpis.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
return (
|
||||||
|
<Grid item key={item.key} xs={6} sm={3}>
|
||||||
|
<StatCard title={item.label} value={item.value} icon={<Icon size={20} />} color={item.color} loading={hubsLoading} />
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* ============================================= || New Hub || ============================================= */}
|
||||||
|
<Stack direction="row" justifyContent="flex-end" sx={{ mb: 1.5 }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<MdAdd size={16} />}
|
||||||
|
onClick={openCreate}
|
||||||
|
sx={{ borderRadius: DT.radiusPill, textTransform: 'none', fontWeight: 700, bgcolor: BRAND, boxShadow: 'none', '&:hover': { bgcolor: '#900E1D', boxShadow: 'none' } }}
|
||||||
|
>
|
||||||
|
New Hub
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{/* ============================================= || Table || ============================================= */}
|
||||||
|
<Paper elevation={0} sx={{ borderRadius: `${DT.radiusCard}px`, border: '1px solid', borderColor: DT.borderSubtle, overflow: 'hidden', background: '#fff' }}>
|
||||||
|
<TableContainer sx={{ maxHeight: { xs: 'calc(100vh - 220px)', md: 'calc(100vh - 190px)' } }}>
|
||||||
|
<Table stickyHeader sx={{ minWidth: 900 }}>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow
|
||||||
|
sx={{
|
||||||
|
'& th': {
|
||||||
|
backgroundColor: DT.surfaceAlt,
|
||||||
|
color: DT.textSecondary,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 800,
|
||||||
|
letterSpacing: 0.6,
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||||||
|
py: 1.25,
|
||||||
|
px: 2
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell>Hub Name</TableCell>
|
||||||
|
<TableCell>City</TableCell>
|
||||||
|
<TableCell>Type</TableCell>
|
||||||
|
<TableCell>Capacity</TableCell>
|
||||||
|
<TableCell>Status</TableCell>
|
||||||
|
<TableCell align="center">Milers Assigned</TableCell>
|
||||||
|
<TableCell align="right">Actions</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{hubsLoading && <OrdersTableSkeleton col={5} />}
|
||||||
|
{!hubsLoading && filteredHubs.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} sx={{ py: 6 }}>
|
||||||
|
<Stack alignItems="center" spacing={1.5}>
|
||||||
|
<Avatar sx={{ width: 64, height: 64, bgcolor: soft('#94a3b8'), color: DT.textMuted }}>
|
||||||
|
<MdOutlineWarehouse size={28} />
|
||||||
|
</Avatar>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||||
|
No hubs to show
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{filteredHubs.map((hub) => {
|
||||||
|
const hubMilers = allMilers.filter((m) => m.hubid === hub.hubid);
|
||||||
|
const expanded = expandedHubId === hub.hubid;
|
||||||
|
return (
|
||||||
|
<Fragment key={hub.hubid}>
|
||||||
|
<TableRow
|
||||||
|
sx={{
|
||||||
|
'& td': { borderBottom: `1px solid ${DT.divider}`, py: 1.5, px: 2 },
|
||||||
|
'&:hover': { backgroundColor: DT.surfaceAlt }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TableCell>
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1}>
|
||||||
|
<AccentAvatar color={BRAND} size={32}>
|
||||||
|
<MdOutlineWarehouse size={16} />
|
||||||
|
</AccentAvatar>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }}>
|
||||||
|
{hub.hubname || '—'}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="body2" sx={{ color: DT.textSecondary }}>
|
||||||
|
{hub.city || '—'}
|
||||||
|
</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={HUB_TYPES.find((t) => t.value === hub.hubtype)?.label || hub.hubtype || '—'}
|
||||||
|
sx={{ bgcolor: tint('#0ea5e9'), color: '#0ea5e9', border: `1px solid ${edge('#0ea5e9')}`, fontWeight: 700 }}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography variant="body2">{hub.capacity ?? '—'}</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={hub.status || 'Active'}
|
||||||
|
sx={{ bgcolor: tint('#10b981'), color: '#10b981', border: `1px solid ${edge('#10b981')}`, fontWeight: 700 }}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="center">
|
||||||
|
<Tooltip title="View milers at this hub">
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
icon={<MdTwoWheeler size={14} />}
|
||||||
|
label={hubMilers.length}
|
||||||
|
onClick={() => setExpandedHubId(expanded ? null : hub.hubid)}
|
||||||
|
sx={{ bgcolor: tint(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}`, fontWeight: 700, cursor: 'pointer' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="right">
|
||||||
|
<Stack direction="row" justifyContent="flex-end" spacing={0.75}>
|
||||||
|
<Tooltip title="Edit hub">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
onClick={() => openEdit(hub)}
|
||||||
|
sx={{ bgcolor: soft(BRAND), color: BRAND, border: `1px solid ${edge(BRAND)}`, '&:hover': { bgcolor: BRAND, color: '#fff' } }}
|
||||||
|
>
|
||||||
|
<MdEdit size={14} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<IconButton size="small" onClick={() => setExpandedHubId(expanded ? null : hub.hubid)}>
|
||||||
|
{expanded ? <MdKeyboardArrowUp size={16} /> : <MdKeyboardArrowDown size={16} />}
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} sx={{ p: 0, border: 0 }}>
|
||||||
|
<Collapse in={expanded} timeout="auto" unmountOnExit>
|
||||||
|
<Box sx={{ p: 2, bgcolor: DT.surfaceAlt }}>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 800, color: DT.textSecondary, textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||||
|
Milers at {hub.hubname}
|
||||||
|
</Typography>
|
||||||
|
{hubMilers.length === 0 ? (
|
||||||
|
<Typography variant="body2" sx={{ color: DT.textMuted, mt: 1 }}>
|
||||||
|
No milers assigned to this hub.
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap sx={{ mt: 1 }}>
|
||||||
|
{hubMilers.map((m) => (
|
||||||
|
<Chip
|
||||||
|
key={m.userid}
|
||||||
|
size="small"
|
||||||
|
icon={<MdTwoWheeler size={13} />}
|
||||||
|
label={`${m.displayname || `Miler #${m.userid}`} · ${m.availabilitystatus || '—'}`}
|
||||||
|
sx={{ bgcolor: '#fff', border: `1px solid ${DT.borderSubtle}`, fontWeight: 600 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Collapse>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* ============================================= || Create / Edit dialog || ============================================= */}
|
||||||
|
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="sm" fullWidth fullScreen={isMobile} PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 } } }}>
|
||||||
|
<DialogTitle sx={{ background: `linear-gradient(135deg, ${BRAND} 0%, #D35968 100%)`, color: '#fff' }}>
|
||||||
|
{editingHub ? `Edit ${editingHub.hubname}` : 'New Hub'}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent sx={{ mt: 2 }}>
|
||||||
|
<Grid container spacing={2.5} sx={{ mt: 0.5 }}>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography sx={{ mb: 1 }}>Hub Name</Typography>
|
||||||
|
<TextField fullWidth value={form.hubname} onChange={(e) => setForm({ ...form, hubname: e.target.value })} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography sx={{ mb: 1 }}>Hub Type</Typography>
|
||||||
|
<Select fullWidth value={form.hubtype} onChange={(e) => setForm({ ...form, hubtype: e.target.value })}>
|
||||||
|
{HUB_TYPES.map((t) => (
|
||||||
|
<MenuItem key={t.value} value={t.value}>
|
||||||
|
{t.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography sx={{ mb: 1 }}>City</Typography>
|
||||||
|
<Select fullWidth value={form.city} onChange={(e) => setForm({ ...form, city: e.target.value })}>
|
||||||
|
{CITIES.map((c) => (
|
||||||
|
<MenuItem key={c} value={c}>
|
||||||
|
{c}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography sx={{ mb: 1 }}>Capacity</Typography>
|
||||||
|
<TextField fullWidth type="number" value={form.capacity} onChange={(e) => setForm({ ...form, capacity: e.target.value })} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography sx={{ mb: 1 }}>Contact</Typography>
|
||||||
|
<TextField fullWidth value={form.contact} onChange={(e) => setForm({ ...form, contact: e.target.value })} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Typography sx={{ mb: 1 }}>Pincode</Typography>
|
||||||
|
<TextField fullWidth value={form.pincode} onChange={(e) => setForm({ ...form, pincode: e.target.value })} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Typography sx={{ mb: 1 }}>Address</Typography>
|
||||||
|
<TextField fullWidth multiline minRows={2} value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ p: 2.5 }}>
|
||||||
|
<Button onClick={() => setDialogOpen(false)} color="secondary" variant="outlined">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
variant="contained"
|
||||||
|
disabled={createMutation.isLoading || updateMutation.isLoading}
|
||||||
|
sx={{ bgcolor: BRAND, '&:hover': { bgcolor: '#900E1D' } }}
|
||||||
|
>
|
||||||
|
{editingHub ? 'Save Changes' : 'Create Hub'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Hubs;
|
||||||
@@ -40,6 +40,10 @@ const EditRider = Loadable(lazy(() => import('pages/nearle/riders/editRider')));
|
|||||||
const Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch')));
|
const Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch')));
|
||||||
const DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview')));
|
const DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview')));
|
||||||
|
|
||||||
|
const Hubs = Loadable(lazy(() => import('pages/nearle/hubs/Hubs')));
|
||||||
|
const BookingDetail = Loadable(lazy(() => import('pages/nearle/bookings/BookingDetail')));
|
||||||
|
const Dashboard = Loadable(lazy(() => import('pages/nearle/dashboard/Dashboard')));
|
||||||
|
|
||||||
|
|
||||||
// ==============================|| MAIN ROUTING ||============================== //
|
// ==============================|| MAIN ROUTING ||============================== //
|
||||||
|
|
||||||
@@ -121,6 +125,18 @@ const MainRoutes = {
|
|||||||
{
|
{
|
||||||
path: 'dispatch/preview',
|
path: 'dispatch/preview',
|
||||||
element: <DispatchPreview />
|
element: <DispatchPreview />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'hubs',
|
||||||
|
element: <Hubs />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'bookings/:id',
|
||||||
|
element: <BookingDetail />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
element: <Dashboard />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,5 +12,7 @@
|
|||||||
"customers": "Customers",
|
"customers": "Customers",
|
||||||
"riders": "Milers",
|
"riders": "Milers",
|
||||||
"dispatch": "Live Operations",
|
"dispatch": "Live Operations",
|
||||||
|
"dashboard": "Dashboard",
|
||||||
|
"hubs": "Hubs",
|
||||||
"Doormile": "Doormile"
|
"Doormile": "Doormile"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user