1115 lines
40 KiB
JavaScript
1115 lines
40 KiB
JavaScript
import * as React from 'react';
|
|
import { enqueueSnackbar } from 'notistack';
|
|
import { useState, useEffect, Fragment, useRef } from 'react';
|
|
import dayjs from 'dayjs';
|
|
var utc = require('dayjs/plugin/utc');
|
|
dayjs.extend(utc);
|
|
import axios from 'axios';
|
|
|
|
import {
|
|
Avatar,
|
|
Box,
|
|
Button,
|
|
Grid,
|
|
IconButton,
|
|
Paper,
|
|
Stack,
|
|
Typography,
|
|
Table,
|
|
TableCell,
|
|
TableBody,
|
|
TableHead,
|
|
Dialog,
|
|
TableRow,
|
|
DialogContent,
|
|
Tooltip,
|
|
Skeleton,
|
|
CircularProgress,
|
|
InputBase,
|
|
Backdrop,
|
|
SpeedDial,
|
|
SpeedDialIcon,
|
|
SpeedDialAction,
|
|
Badge,
|
|
TableContainer,
|
|
Checkbox
|
|
} from '@mui/material';
|
|
import {
|
|
MdAccessTime,
|
|
MdCancel,
|
|
MdCheckCircle,
|
|
MdClose,
|
|
MdCurrencyRupee,
|
|
MdDeleteOutline,
|
|
MdHistoryToggleOff,
|
|
MdHourglassEmpty,
|
|
MdInventory2,
|
|
MdLocalShipping,
|
|
MdSearch,
|
|
MdStraighten,
|
|
MdCalendarMonth,
|
|
MdReceiptLong,
|
|
MdClear,
|
|
MdNotes
|
|
} from 'react-icons/md';
|
|
import { DeleteOutlined } from '@ant-design/icons';
|
|
import Loader from 'components/Loader';
|
|
import { useHotkeyFocus } from 'components/nearle_components/useHotkeyFocus';
|
|
import DateFilterDialog from 'components/nearle_components/DateFilterDialog';
|
|
import CircularLoader from 'components/nearle_components/CircularLoader';
|
|
import AiImage from '../../../assets/images/aiImage.png';
|
|
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
|
|
import { useNavigate, useLocation } from 'react-router-dom';
|
|
import {
|
|
fetchPercentageData,
|
|
createAutomationDeliveries,
|
|
cancelMultipleOrder,
|
|
getallriders,
|
|
fetchorderscount
|
|
} from '../api/api';
|
|
|
|
// ============================================================================
|
|
// Design tokens — shared with the rest of the redesigned operator pages.
|
|
// ============================================================================
|
|
const DT = {
|
|
radiusPill: 999,
|
|
radiusCard: 16,
|
|
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 dtA = (c, suffix) => `${c}${suffix}`;
|
|
const tint = (c) => dtA(c, '08');
|
|
const soft = (c) => dtA(c, '18');
|
|
const ring = (c) => dtA(c, '26');
|
|
const edge = (c) => dtA(c, '55');
|
|
|
|
const dtTint = tint;
|
|
const dtSoft = soft;
|
|
const dtRing = ring;
|
|
const dtEdge = edge;
|
|
|
|
const BRAND = '#662582';
|
|
const BRAND_LIGHT = '#9255AB';
|
|
|
|
|
|
// Semantic per-row status palette — colors per brand standard:
|
|
// green=delivered, amber=pending, blue=created/processing, red=cancelled,
|
|
// dark-red=failed, purple=on-hold.
|
|
const ROW_STATUS_META = {
|
|
created: { label: 'Created', color: '#3b82f6', icon: MdLocalShipping },
|
|
pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
|
processing: { label: 'Processing', color: '#3b82f6', icon: MdAccessTime },
|
|
modified: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
|
|
confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
|
|
ready: { label: 'Accepted', color: '#6366f1', icon: MdCheckCircle },
|
|
active: { label: 'Picked', color: '#8b5cf6', icon: MdLocalShipping },
|
|
onhold: { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff },
|
|
closed: { label: 'Closed', color: '#06b6d4', icon: MdCheckCircle },
|
|
delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
|
|
failed: { label: 'Failed', color: '#991b1b', icon: MdCancel },
|
|
cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
|
|
};
|
|
|
|
// Top-level pill tabs.
|
|
const ORDERS_STATUS_TABS = [
|
|
{ idx: 0, status: 'created', label: 'Created', color: BRAND, icon: MdLocalShipping, countKey: 'created' }
|
|
];
|
|
|
|
const StatusBadge = ({ status }) => {
|
|
const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || {
|
|
label: status || '—',
|
|
color: DT.textMuted,
|
|
icon: MdHistoryToggleOff
|
|
};
|
|
const Icon = meta.icon;
|
|
return (
|
|
<Box
|
|
sx={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: 0.5,
|
|
px: 1.125,
|
|
py: 0.375,
|
|
borderRadius: 999,
|
|
bgcolor: meta.color,
|
|
color: '#fff',
|
|
fontSize: 11,
|
|
fontWeight: 700,
|
|
letterSpacing: 0.2,
|
|
whiteSpace: 'nowrap',
|
|
minWidth: 86,
|
|
justifyContent: 'center',
|
|
boxShadow: `0 1px 2px ${ring(meta.color)}`
|
|
}}
|
|
>
|
|
<Icon size={12} /> {meta.label}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const MetricCell = ({ value, color, icon, isMoney = false }) => {
|
|
const n = Number(value);
|
|
const display = isMoney ? `₹${Number.isFinite(n) ? n.toFixed(2) : '0.00'}` : Number.isFinite(n) ? n : value || 0;
|
|
const isZero = !Number.isFinite(n) || n === 0;
|
|
if (isZero) {
|
|
return (
|
|
<Typography variant="caption" sx={{ color: DT.textMuted, fontWeight: 700 }}>
|
|
{display}
|
|
</Typography>
|
|
);
|
|
}
|
|
return (
|
|
<Box
|
|
sx={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: 0.5,
|
|
px: 0.875,
|
|
py: 0.25,
|
|
borderRadius: 999,
|
|
bgcolor: tint(color),
|
|
border: `1px solid ${edge(color)}`,
|
|
color,
|
|
fontSize: 12,
|
|
fontWeight: 800,
|
|
whiteSpace: 'nowrap'
|
|
}}
|
|
>
|
|
{icon}
|
|
{display}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const Orders = () => {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const tid = localStorage.getItem('tenantid');
|
|
const loadMoreRef = useRef();
|
|
const containerRef = useRef();
|
|
const [page, setPage] = useState(0);
|
|
const [rowsPerPage, setRowsPerPage] = useState(10);
|
|
const [tabvalue, setTabvalue] = useState(0);
|
|
const [tabstatus, setTabstatus] = useState('Created');
|
|
const [currentStatus, setCurrentStatus] = useState('created');
|
|
const [cancelOpen, setCancelOpen] = useState(false);
|
|
const [orderheaderid, setOrderheaderid] = useState('');
|
|
const locationId = 0;
|
|
const locoName = 'All Locations';
|
|
const [dateOpen, setDateOpen] = useState(false);
|
|
const [datestatus, setDatestatus] = useState('Today');
|
|
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
|
|
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
|
|
const [searchword, setSearchword] = useState('');
|
|
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
|
|
// Floating button and Dialog states
|
|
const [speedDialOpen, setSpeedDialOpen] = useState(false);
|
|
const [multiDeleteDialog, setMultiDeleteDialog] = useState(false);
|
|
const [createloader, setCreateloader] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [deliverylist, setDeliverylist] = useState([]);
|
|
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => {
|
|
setDebouncedSearch(searchword);
|
|
}, 400);
|
|
return () => clearTimeout(handler);
|
|
}, [searchword]);
|
|
|
|
const handleChangetab = (e, i) => {
|
|
setSearchword('');
|
|
setRowsPerPage(10);
|
|
setTabvalue(i);
|
|
const tab = ORDERS_STATUS_TABS[i];
|
|
setTabstatus(tab.label);
|
|
setCurrentStatus(tab.status);
|
|
setPage(0);
|
|
};
|
|
|
|
const textFieldRef = useRef(null);
|
|
useHotkeyFocus(textFieldRef, 'k');
|
|
|
|
// React Queries
|
|
const {
|
|
data: percentageData,
|
|
isLoading: fetchpercentageIsLoading,
|
|
refetch: percentagedataRefetch
|
|
} = useQuery({
|
|
queryKey: ['percentageData', locationId, startdate, enddate, tid, locationId],
|
|
queryFn: fetchPercentageData,
|
|
enabled: true,
|
|
refetchInterval: 15000
|
|
});
|
|
|
|
const {
|
|
data: ordersCountData,
|
|
refetch: orderscountRefetch
|
|
} = useQuery({
|
|
queryKey: ['ordersCount', locationId, startdate, enddate, currentStatus, tid, locationId],
|
|
queryFn: fetchorderscount,
|
|
refetchOnMount: true,
|
|
refetchOnWindowFocus: true,
|
|
refetchInterval: 15000
|
|
});
|
|
|
|
const {
|
|
data: autoRiders
|
|
} = useQuery({
|
|
queryKey: ['getallriders'],
|
|
queryFn: getallriders,
|
|
refetchOnMount: true,
|
|
refetchOnWindowFocus: true
|
|
});
|
|
|
|
const cancelorder = async () => {
|
|
await axios
|
|
.put(`${process.env.REACT_APP_URL}/orders/updateorder`, {
|
|
orderheaderid: orderheaderid,
|
|
orderstatus: 'cancelled',
|
|
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss')
|
|
})
|
|
.then((res) => {
|
|
if (res.data.status) {
|
|
enqueueSnackbar('Order Cancelled Successfully', {
|
|
variant: 'success',
|
|
anchorOrigin: { vertical: 'top', horizontal: 'right' },
|
|
autoHideDuration: 2000
|
|
});
|
|
refetchOrders();
|
|
orderscountRefetch();
|
|
percentagedataRefetch();
|
|
setCancelOpen(false);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.log(err);
|
|
});
|
|
};
|
|
|
|
const fetchOrders = async ({ pageParam = 1 }) => {
|
|
const res = await axios.get(
|
|
`${process.env.REACT_APP_URL}/orders/tenant/getorders/?tenantid=${tid}&locationid=${locationId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${debouncedSearch}`
|
|
);
|
|
return {
|
|
data: res.data.details,
|
|
nextPage: res.data.details.length === rowsPerPage ? pageParam + 1 : undefined
|
|
};
|
|
};
|
|
|
|
const {
|
|
data: rowdata,
|
|
fetchNextPage,
|
|
isLoading: isLoadingGetOrders,
|
|
hasNextPage,
|
|
isFetchingNextPage,
|
|
refetch: refetchOrders
|
|
} = useInfiniteQuery({
|
|
queryKey: [tabstatus, startdate, enddate, page, rowsPerPage, debouncedSearch, locationId],
|
|
queryFn: fetchOrders,
|
|
getNextPageParam: (lastPage) => lastPage.nextPage,
|
|
refetchInterval: 15000
|
|
});
|
|
|
|
const rows = rowdata ? rowdata.pages.flatMap((p) => p.data) : [];
|
|
|
|
useEffect(() => {
|
|
if (!hasNextPage) return;
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries[0].isIntersecting) {
|
|
fetchNextPage();
|
|
}
|
|
},
|
|
{
|
|
root: null,
|
|
rootMargin: '0px 0px 400px 0px',
|
|
threshold: 0
|
|
}
|
|
);
|
|
if (loadMoreRef.current) observer.observe(loadMoreRef.current);
|
|
return () => {
|
|
if (loadMoreRef.current) observer.unobserve(loadMoreRef.current);
|
|
};
|
|
}, [hasNextPage, fetchNextPage]);
|
|
|
|
const handleScroll = (event) => {
|
|
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
|
|
if (scrollTop + clientHeight >= scrollHeight - 50) {
|
|
if (hasNextPage && !isFetchingNextPage) {
|
|
fetchNextPage();
|
|
}
|
|
}
|
|
};
|
|
|
|
// ==============================|| Mutations ||============================== //
|
|
|
|
const createDeliveryMutation = useMutation({
|
|
mutationFn: createAutomationDeliveries,
|
|
onSuccess: (data, variables) => {
|
|
enqueueSnackbar('Orders Optimised Successfully', { variant: 'success', autoHideDuration: 2000, anchorOrigin: { vertical: 'top', horizontal: 'right' } });
|
|
orderscountRefetch();
|
|
refetchOrders();
|
|
setCreateloader(false);
|
|
navigate('/nearle/dispatch/preview', {
|
|
state: {
|
|
dispatchPreviewData: data,
|
|
aiMode: 1,
|
|
selectedMode: { value: 1 },
|
|
deliveryData: variables?.deliveries || [],
|
|
appId: locationId,
|
|
startdate: startdate,
|
|
tenantId: tid,
|
|
autoRiders: autoRiders || []
|
|
}
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 4000 });
|
|
setCreateloader(false);
|
|
},
|
|
onSettled: () => {
|
|
setCreateloader(false);
|
|
setIsLoading(false);
|
|
}
|
|
});
|
|
|
|
const cancelMultipleOrderMutation = useMutation({
|
|
mutationFn: cancelMultipleOrder,
|
|
onSuccess: (data) => {
|
|
if (data.status) {
|
|
setMultiDeleteDialog(false);
|
|
enqueueSnackbar('Orders Cancelled Successfully', { variant: 'success', autoHideDuration: 2000 });
|
|
refetchOrders();
|
|
orderscountRefetch();
|
|
percentagedataRefetch();
|
|
setDeliverylist([]);
|
|
}
|
|
},
|
|
onError: (error) => {
|
|
enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 4000 });
|
|
}
|
|
});
|
|
|
|
const handleCreateDelivery = async () => {
|
|
if (rows.length === 0) return;
|
|
setIsLoading(true);
|
|
setCreateloader(true);
|
|
const deliveryData = rows.map((val) => ({
|
|
...val,
|
|
deliveryid: 0,
|
|
deliverydate: dayjs(val.deliverydate).utc().format('YYYY-MM-DD HH:mm:ss'),
|
|
assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
|
orderstatus: 'pending',
|
|
orderamount: val.deliverycharge,
|
|
droplat: val.deliverylat,
|
|
droplon: val.deliverylong,
|
|
pickuplat: val.pickuplat,
|
|
pickuplon: val.pickuplong,
|
|
ordernotes: val.ordernotes,
|
|
deliverycharges: val.deliverycharge,
|
|
pickuplocation: val.pickupsuburb,
|
|
deliverylocation: val.deliverysuburb
|
|
}));
|
|
|
|
createDeliveryMutation.mutate({
|
|
deliveries: deliveryData,
|
|
selectedMode: { value: 1 },
|
|
hypertuning_params: 'balanced',
|
|
absent_riders: []
|
|
});
|
|
};
|
|
|
|
// KPI tile definitions.
|
|
const kpiCards = [
|
|
{ key: 'created', label: 'Created Orders', color: BRAND, icon: MdLocalShipping, value: percentageData?.created, percentage: percentageData?.percentage1 },
|
|
{ key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdHourglassEmpty, value: percentageData?.uncoveredOrders, percentage: percentageData?.percentage2 }
|
|
];
|
|
|
|
return (
|
|
<Fragment>
|
|
{(fetchpercentageIsLoading || isLoadingGetOrders || isLoading || createloader) && (
|
|
<>
|
|
<Loader />
|
|
<CircularLoader />
|
|
</>
|
|
)}
|
|
|
|
{rows.length > 0 && currentStatus === 'created' && (
|
|
<Button
|
|
variant="contained"
|
|
onClick={handleCreateDelivery}
|
|
sx={{
|
|
position: 'fixed',
|
|
bottom: 32,
|
|
right: 24,
|
|
bgcolor: BRAND,
|
|
color: '#fff',
|
|
px: 3,
|
|
py: 1.5,
|
|
borderRadius: 999,
|
|
fontWeight: 850,
|
|
boxShadow: `0 10px 30px ${ring(BRAND)}`,
|
|
zIndex: 1000,
|
|
textTransform: 'none',
|
|
fontSize: '14px',
|
|
gap: 1,
|
|
'&:hover': {
|
|
bgcolor: BRAND_LIGHT,
|
|
transform: 'translateY(-2px)'
|
|
}
|
|
}}
|
|
startIcon={<MdLocalShipping size={18} />}
|
|
>
|
|
Assign Orders ({rows.length})
|
|
</Button>
|
|
)}
|
|
|
|
{/* ============================================= || Header (compact) || ============================================= */}
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
mb: { xs: 1, md: 1.25 },
|
|
px: { xs: 1.5, sm: 2 },
|
|
py: { xs: 1, sm: 1.25 },
|
|
borderRadius: 2,
|
|
border: '1px solid',
|
|
borderColor: DT.borderSubtle,
|
|
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
|
|
boxShadow: DT.shadowMd
|
|
}}
|
|
>
|
|
<Stack
|
|
direction={{ xs: 'column', sm: 'row' }}
|
|
alignItems={{ xs: 'flex-start', sm: 'center' }}
|
|
justifyContent="space-between"
|
|
spacing={{ xs: 1, sm: 1.5 }}
|
|
>
|
|
<Stack direction="row" alignItems="center" spacing={1.25}>
|
|
<Avatar
|
|
variant="rounded"
|
|
sx={{
|
|
width: 36,
|
|
height: 36,
|
|
bgcolor: BRAND,
|
|
color: '#fff',
|
|
borderRadius: 1.5,
|
|
boxShadow: `0 4px 12px ${ring(BRAND)}`
|
|
}}
|
|
>
|
|
<MdLocalShipping size={19} />
|
|
</Avatar>
|
|
<Stack spacing={0.125}>
|
|
<Typography
|
|
variant="h3"
|
|
sx={{
|
|
fontWeight: 800,
|
|
color: DT.textPrimary,
|
|
lineHeight: 1.1,
|
|
fontSize: { xs: '1.1rem', sm: '1.25rem', md: '1.375rem' }
|
|
}}
|
|
>
|
|
Orders
|
|
</Typography>
|
|
<Stack direction="row" alignItems="center" spacing={0.75}>
|
|
<Box
|
|
sx={{
|
|
width: 7,
|
|
height: 7,
|
|
borderRadius: '50%',
|
|
bgcolor: '#10b981',
|
|
boxShadow: '0 0 0 3px rgba(16,185,129,0.18)'
|
|
}}
|
|
/>
|
|
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 600 }}>
|
|
Live · {locoName} · {datestatus}
|
|
</Typography>
|
|
</Stack>
|
|
</Stack>
|
|
</Stack>
|
|
|
|
</Stack>
|
|
</Paper>
|
|
|
|
|
|
|
|
|
|
|
|
{/* ============================================= || Status Tabs + Search (compact) || ============================================= */}
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
mt: { xs: 1, md: 1.25 },
|
|
p: { xs: 0.875, md: 1.125 },
|
|
borderTopLeftRadius: 2,
|
|
borderTopRightRadius: 2,
|
|
borderBottomLeftRadius: 0,
|
|
borderBottomRightRadius: 0,
|
|
border: '1px solid',
|
|
borderColor: DT.borderSubtle,
|
|
borderBottom: 0,
|
|
background: '#fff'
|
|
}}
|
|
>
|
|
<Stack
|
|
direction="row"
|
|
alignItems="center"
|
|
justifyContent="space-between"
|
|
gap={1.5}
|
|
sx={{ flexWrap: 'wrap-reverse' }}
|
|
>
|
|
<Stack
|
|
direction="row"
|
|
spacing={0.75}
|
|
sx={{
|
|
flex: 1,
|
|
overflowX: 'auto',
|
|
py: 0.5,
|
|
px: 0.25,
|
|
'&::-webkit-scrollbar': { height: 6 },
|
|
'&::-webkit-scrollbar-thumb': { backgroundColor: DT.borderSubtle, borderRadius: 4 }
|
|
}}
|
|
>
|
|
{ORDERS_STATUS_TABS.map((t) => {
|
|
const Icon = t.icon;
|
|
const active = tabvalue === t.idx;
|
|
const count = ordersCountData?.[t.countKey] ?? 0;
|
|
return (
|
|
<Box
|
|
key={t.status}
|
|
onClick={(e) => handleChangetab(e, t.idx)}
|
|
sx={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: { xs: 0.625, md: 0.875 },
|
|
pl: 0.5,
|
|
pr: { xs: 1, md: 1.25 },
|
|
py: 0.5,
|
|
flexShrink: 0,
|
|
cursor: 'pointer',
|
|
borderRadius: 999,
|
|
border: `1.5px solid ${active ? t.color : edge(t.color)}`,
|
|
bgcolor: active ? t.color : tint(t.color),
|
|
color: active ? '#fff' : t.color,
|
|
fontWeight: 700,
|
|
boxShadow: active ? `0 6px 18px ${ring(t.color)}` : 'none',
|
|
transition: 'all 0.18s',
|
|
'&:hover': {
|
|
borderColor: t.color,
|
|
boxShadow: active ? `0 6px 18px ${ring(t.color)}` : `0 0 0 3px ${ring(t.color)}`
|
|
}
|
|
}}
|
|
>
|
|
<Avatar
|
|
sx={{
|
|
width: { xs: 22, md: 26 },
|
|
height: { xs: 22, md: 26 },
|
|
bgcolor: active ? 'rgba(255,255,255,0.22)' : soft(t.color),
|
|
color: active ? '#fff' : t.color
|
|
}}
|
|
>
|
|
<Icon size={13} />
|
|
</Avatar>
|
|
<Typography variant="caption" sx={{ fontWeight: 800, fontSize: { xs: 11.5, md: 13 }, lineHeight: 1 }}>
|
|
{t.label}
|
|
</Typography>
|
|
<Box
|
|
sx={{
|
|
minWidth: { xs: 22, md: 26 },
|
|
height: { xs: 18, md: 22 },
|
|
px: 0.625,
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
borderRadius: 999,
|
|
fontSize: { xs: 10, md: 11 },
|
|
fontWeight: 800,
|
|
bgcolor: active ? 'rgba(255,255,255,0.22)' : '#fff',
|
|
color: active ? '#fff' : t.color,
|
|
border: active ? 'none' : `1px solid ${edge(t.color)}`
|
|
}}
|
|
>
|
|
{count ?? 0}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
})}
|
|
</Stack>
|
|
|
|
</Stack>
|
|
</Paper>
|
|
|
|
{/* ============================================= || Table (dense, sticky header) || ============================================= */}
|
|
<Paper
|
|
elevation={0}
|
|
sx={{
|
|
borderTopLeftRadius: 0,
|
|
borderTopRightRadius: 0,
|
|
borderBottomLeftRadius: 2,
|
|
borderBottomRightRadius: 2,
|
|
border: '1px solid',
|
|
borderColor: DT.borderSubtle,
|
|
overflow: 'hidden',
|
|
background: '#fff'
|
|
}}
|
|
>
|
|
<TableContainer
|
|
onScroll={handleScroll}
|
|
ref={containerRef}
|
|
sx={{
|
|
overflowX: 'auto',
|
|
overflowY: 'visible',
|
|
'&::-webkit-scrollbar': { width: 10, height: 10 },
|
|
'&::-webkit-scrollbar-thumb': {
|
|
backgroundColor: edge(BRAND),
|
|
borderRadius: 8,
|
|
'&:hover': { backgroundColor: BRAND }
|
|
},
|
|
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
|
}}
|
|
>
|
|
<Table stickyHeader size="small" sx={{ minWidth: { xs: 960, md: 1100 } }}>
|
|
<TableHead>
|
|
<TableRow
|
|
sx={{
|
|
'& th': {
|
|
backgroundColor: DT.surfaceAlt,
|
|
color: DT.textSecondary,
|
|
fontSize: 10.5,
|
|
fontWeight: 800,
|
|
letterSpacing: 0.5,
|
|
textTransform: 'uppercase',
|
|
whiteSpace: 'nowrap',
|
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
|
py: 0.75,
|
|
px: 1
|
|
}
|
|
}}
|
|
>
|
|
<TableCell>#</TableCell>
|
|
<TableCell>Order Location</TableCell>
|
|
<TableCell>Pickup</TableCell>
|
|
<TableCell>Drop</TableCell>
|
|
<TableCell align="center">Qty</TableCell>
|
|
<TableCell align="right">COD</TableCell>
|
|
<TableCell align="center">Kms</TableCell>
|
|
<TableCell align="right">Charges</TableCell>
|
|
<TableCell>Notes</TableCell>
|
|
<TableCell>Status</TableCell>
|
|
{currentStatus === 'created' && <TableCell align="right">Actions</TableCell>}
|
|
</TableRow>
|
|
</TableHead>
|
|
|
|
<TableBody>
|
|
{(isLoadingGetOrders || createloader) &&
|
|
rows.length === 0 &&
|
|
Array.from({ length: 10 }).map((_, idx) => (
|
|
<TableRow key={`sk-${idx}`}>
|
|
{Array.from({ length: currentStatus === 'created' ? 12 : 11 }).map((__, ci) => (
|
|
<TableCell key={ci} sx={{ borderBottom: `1px solid ${DT.divider}`, py: 0.625, px: 1 }}>
|
|
<Skeleton animation="wave" height={20} />
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
|
|
{!isLoadingGetOrders && rows.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={currentStatus === 'created' ? 12 : 11} sx={{ py: 7, borderBottom: 'none' }}>
|
|
<Stack alignItems="center" spacing={1.25}>
|
|
<Avatar
|
|
variant="rounded"
|
|
sx={{
|
|
width: 56,
|
|
height: 56,
|
|
bgcolor: soft('#94a3b8'),
|
|
color: DT.textMuted,
|
|
borderRadius: 2
|
|
}}
|
|
>
|
|
<MdLocalShipping size={26} />
|
|
</Avatar>
|
|
<Typography sx={{ fontWeight: 700, color: DT.textPrimary, fontSize: 14 }}>
|
|
No {currentStatus} orders
|
|
</Typography>
|
|
<Typography sx={{ color: DT.textSecondary, fontSize: 12 }}>
|
|
{searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'}
|
|
</Typography>
|
|
{searchword && (
|
|
<Button
|
|
size="small"
|
|
onClick={() => setSearchword('')}
|
|
sx={{
|
|
mt: 0.5,
|
|
height: 28,
|
|
textTransform: 'none',
|
|
fontSize: 12,
|
|
fontWeight: 700,
|
|
color: BRAND,
|
|
borderRadius: 1.25,
|
|
'&:hover': { bgcolor: tint(BRAND) }
|
|
}}
|
|
startIcon={<MdClear size={14} />}
|
|
>
|
|
Clear search
|
|
</Button>
|
|
)}
|
|
</Stack>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
|
|
{rows.map((row, index) => {
|
|
const isItemSelected = !!deliverylist.find((res) => res.orderheaderid === row.orderheaderid);
|
|
const handleCheckbox = (e) => {
|
|
if (e.target.checked) {
|
|
setDeliverylist((prev) => [...prev, { ...row, sno: prev.length + 1 }]);
|
|
} else {
|
|
setDeliverylist((prev) =>
|
|
prev.filter((item) => item.orderheaderid !== row.orderheaderid).map((item, i) => ({ ...item, sno: i + 1 }))
|
|
);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<TableRow
|
|
key={`${row.orderheaderid}-${index}`}
|
|
sx={{
|
|
cursor: 'pointer',
|
|
transition: 'background-color 0.12s, box-shadow 0.12s',
|
|
backgroundColor: 'transparent',
|
|
'& td': {
|
|
borderBottom: `1px solid ${DT.divider}`,
|
|
py: 0.5,
|
|
px: 1,
|
|
verticalAlign: 'top'
|
|
},
|
|
'&:hover': {
|
|
backgroundColor: DT.surfaceAlt,
|
|
boxShadow: `inset 3px 0 0 ${BRAND}`
|
|
}
|
|
}}
|
|
>
|
|
<TableCell>
|
|
<Typography sx={{ fontWeight: 700, color: DT.textSecondary }}>
|
|
{page * rowsPerPage + index + 1}
|
|
</Typography>
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
|
{row.locationname}
|
|
{row.locationsuburb && ` - ${row.locationsuburb}`}
|
|
</Typography>
|
|
<Tooltip title="Order Id">
|
|
<Typography variant="caption" sx={{ color: DT.textSecondary }} noWrap>
|
|
{row.orderid}
|
|
</Typography>
|
|
</Tooltip>
|
|
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ mt: 0.125 }}>
|
|
<MdAccessTime size={10} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
|
{(() => {
|
|
const dateObj = row.pickupslot && dayjs(row.pickupslot).isValid()
|
|
? dayjs(row.pickupslot)
|
|
: dayjs(row.deliverydate || row.orderdate);
|
|
return (
|
|
<>
|
|
<Typography sx={{ fontSize: 10.5, color: DT.textSecondary, fontWeight: 700 }} noWrap>
|
|
{dateObj.format('hh:mm A')}
|
|
</Typography>
|
|
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600 }} noWrap>
|
|
· {dateObj.format('DD MMM YY')}
|
|
</Typography>
|
|
</>
|
|
);
|
|
})()}
|
|
</Stack>
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<Stack direction="column">
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} noWrap>
|
|
{row.pickupcustomer}
|
|
</Typography>
|
|
<Typography variant="caption" sx={{ color: DT.textSecondary }}>
|
|
{row.pickupcontactno}
|
|
</Typography>
|
|
<Tooltip title={row.pickupaddress}>
|
|
<Typography variant="caption" sx={{ color: DT.textMuted }} noWrap>
|
|
{row.pickupsuburb || (row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—')}
|
|
</Typography>
|
|
</Tooltip>
|
|
</Stack>
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<Stack direction="column">
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 700, color: DT.textPrimary }} 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 }} noWrap>
|
|
{row.deliverysuburb ||
|
|
(row.deliveryaddress?.length > 20 ? `${row.deliveryaddress.slice(0, 20)}…` : row.deliveryaddress || '—')}
|
|
</Typography>
|
|
</Tooltip>
|
|
</Stack>
|
|
</TableCell>
|
|
|
|
<TableCell align="center">
|
|
<MetricCell value={row.quantity} color="#0ea5e9" icon={<MdInventory2 size={11} />} />
|
|
</TableCell>
|
|
|
|
<TableCell align="right">
|
|
<MetricCell value={row.collectionamt} color="#10b981" icon={<MdCurrencyRupee size={11} />} isMoney />
|
|
</TableCell>
|
|
|
|
<TableCell align="center">
|
|
<MetricCell value={row.kms} color="#f59e0b" icon={<MdStraighten size={11} />} />
|
|
</TableCell>
|
|
|
|
<TableCell align="right">
|
|
<MetricCell value={row.deliverycharge} color={BRAND} icon={<MdCurrencyRupee size={11} />} isMoney />
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
{row.ordernotes ? (
|
|
<Stack direction="row" spacing={0.5} alignItems="center" sx={{ color: DT.textSecondary }}>
|
|
<MdNotes size={12} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
|
<Typography
|
|
variant="caption"
|
|
sx={{
|
|
maxWidth: 140,
|
|
whiteSpace: 'nowrap',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
fontWeight: 600
|
|
}}
|
|
>
|
|
{row.ordernotes}
|
|
</Typography>
|
|
</Stack>
|
|
) : (
|
|
<Typography variant="caption" sx={{ color: DT.textMuted }}>
|
|
—
|
|
</Typography>
|
|
)}
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
<StatusBadge status={row.orderstatus} />
|
|
</TableCell>
|
|
|
|
{currentStatus === 'created' && (
|
|
<TableCell align="right">
|
|
{row.orderstatus === 'created' && (
|
|
<Tooltip title="Cancel Order">
|
|
<IconButton
|
|
size="small"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setOrderheaderid(row.orderheaderid);
|
|
setCancelOpen(true);
|
|
}}
|
|
sx={{
|
|
bgcolor: tint('#ef4444'),
|
|
border: `1px solid ${edge('#ef4444')}`,
|
|
color: '#ef4444',
|
|
borderRadius: 999,
|
|
p: 0.75,
|
|
'&:hover': {
|
|
bgcolor: soft('#ef4444'),
|
|
borderColor: '#ef4444'
|
|
}
|
|
}}
|
|
>
|
|
<MdClose size={14} />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
</TableCell>
|
|
)}
|
|
</TableRow>
|
|
);
|
|
})}
|
|
|
|
{rows.length !== 0 && (
|
|
<TableRow sx={{ '&:hover': { backgroundColor: 'transparent !important' } }}>
|
|
<TableCell colSpan={currentStatus === 'created' ? 12 : 11} sx={{ borderBottom: 'none', py: 1, bgcolor: DT.surfaceAlt }}>
|
|
<Stack
|
|
ref={loadMoreRef}
|
|
direction="row"
|
|
alignItems="center"
|
|
justifyContent="center"
|
|
spacing={1}
|
|
sx={{ minHeight: 32 }}
|
|
>
|
|
{isFetchingNextPage || hasNextPage ? (
|
|
<>
|
|
<CircularProgress size={14} sx={{ color: BRAND }} />
|
|
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 700 }}>
|
|
Loading more orders…
|
|
</Typography>
|
|
</>
|
|
) : (
|
|
<Typography sx={{ fontSize: 11.5, color: DT.textMuted, fontWeight: 700, letterSpacing: 0.3 }}>
|
|
{rows.length} order{rows.length === 1 ? '' : 's'} · End of list
|
|
</Typography>
|
|
)}
|
|
</Stack>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
</Paper>
|
|
|
|
{/* ============================================= || Cancel Order Dialog || ============================================= */}
|
|
<Dialog open={cancelOpen} onClose={() => setCancelOpen(false)} maxWidth="xs" PaperProps={{ sx: { borderRadius: 3 } }}>
|
|
<Box
|
|
sx={{
|
|
p: 2.5,
|
|
background: `linear-gradient(135deg, ${tint('#ef4444')} 0%, ${tint('#f59e0b')} 100%)`,
|
|
borderBottom: `1px solid ${DT.borderSubtle}`
|
|
}}
|
|
>
|
|
<Stack direction="row" alignItems="center" spacing={1.5}>
|
|
<Avatar sx={{ bgcolor: '#ef4444', color: '#fff', width: 40, height: 40 }}>
|
|
<MdDeleteOutline size={20} />
|
|
</Avatar>
|
|
<Typography variant="h5" sx={{ fontWeight: 800, color: DT.textPrimary }}>
|
|
Cancel Order
|
|
</Typography>
|
|
</Stack>
|
|
</Box>
|
|
<DialogContent sx={{ pt: 3 }}>
|
|
<Stack alignItems="center" spacing={3}>
|
|
<Typography variant="body1" align="center" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
|
Are you sure you want to cancel this order? This action cannot be undone.
|
|
</Typography>
|
|
<Stack direction="row" spacing={1.5} sx={{ width: 1 }}>
|
|
<Button
|
|
fullWidth
|
|
onClick={() => setCancelOpen(false)}
|
|
variant="outlined"
|
|
sx={{
|
|
borderRadius: 999,
|
|
py: 1,
|
|
borderColor: DT.borderSubtle,
|
|
color: DT.textSecondary,
|
|
fontWeight: 700,
|
|
'&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
|
|
}}
|
|
>
|
|
No
|
|
</Button>
|
|
<Button
|
|
fullWidth
|
|
variant="contained"
|
|
onClick={cancelorder}
|
|
autoFocus
|
|
sx={{
|
|
borderRadius: 999,
|
|
py: 1,
|
|
bgcolor: '#ef4444',
|
|
fontWeight: 700,
|
|
boxShadow: `0 6px 18px ${ring('#ef4444')}`,
|
|
'&:hover': { bgcolor: '#dc2626' }
|
|
}}
|
|
>
|
|
Yes, Cancel
|
|
</Button>
|
|
</Stack>
|
|
</Stack>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* ============================================= || Date Filter || ============================================= */}
|
|
<DateFilterDialog
|
|
open={dateOpen}
|
|
onClose={() => setDateOpen(false)}
|
|
onApply={({ startDate, endDate, label }) => {
|
|
setStartdate(startDate);
|
|
setEnddate(endDate);
|
|
setDatestatus(label);
|
|
}}
|
|
/>
|
|
|
|
{/* ============================================= || Cancel Multiple Orders Dialog || ============================================= */}
|
|
<Dialog open={multiDeleteDialog} onClose={() => setMultiDeleteDialog(false)} maxWidth="xs" PaperProps={{ sx: { borderRadius: 3 } }}>
|
|
<Box
|
|
sx={{
|
|
p: 2.5,
|
|
background: `linear-gradient(135deg, ${tint('#ef4444')} 0%, ${tint('#f59e0b')} 100%)`,
|
|
borderBottom: `1px solid ${DT.borderSubtle}`
|
|
}}
|
|
>
|
|
<Stack direction="row" alignItems="center" spacing={1.5}>
|
|
<Avatar sx={{ bgcolor: '#ef4444', color: '#fff', width: 40, height: 40 }}>
|
|
<MdDeleteOutline size={20} />
|
|
</Avatar>
|
|
<Typography variant="h5" sx={{ fontWeight: 800, color: DT.textPrimary }}>
|
|
Cancel Selected Orders
|
|
</Typography>
|
|
</Stack>
|
|
</Box>
|
|
<DialogContent sx={{ pt: 3 }}>
|
|
<Stack alignItems="center" spacing={3}>
|
|
<Typography variant="body1" align="center" sx={{ color: DT.textSecondary, fontWeight: 600 }}>
|
|
Are you sure you want to cancel the {deliverylist.length} selected orders? This action cannot be undone.
|
|
</Typography>
|
|
<Stack direction="row" spacing={1.5} sx={{ width: 1 }}>
|
|
<Button
|
|
fullWidth
|
|
onClick={() => setMultiDeleteDialog(false)}
|
|
variant="outlined"
|
|
sx={{
|
|
borderRadius: 999,
|
|
py: 1,
|
|
borderColor: DT.borderSubtle,
|
|
color: DT.textSecondary,
|
|
fontWeight: 700,
|
|
'&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
|
|
}}
|
|
>
|
|
No
|
|
</Button>
|
|
<Button
|
|
fullWidth
|
|
variant="contained"
|
|
onClick={() => cancelMultipleOrderMutation.mutate(deliverylist)}
|
|
autoFocus
|
|
sx={{
|
|
borderRadius: 999,
|
|
py: 1,
|
|
bgcolor: '#ef4444',
|
|
fontWeight: 700,
|
|
boxShadow: `0 6px 18px ${ring('#ef4444')}`,
|
|
'&:hover': { bgcolor: '#dc2626' }
|
|
}}
|
|
>
|
|
Yes, Cancel
|
|
</Button>
|
|
</Stack>
|
|
</Stack>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
export default Orders;
|