Files
dailygrubs_console/src/pages/nearle/orders/orders.js

1318 lines
48 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { useTheme } from '@mui/material/styles';
import {
Avatar,
Box,
Button,
Grid,
IconButton,
Paper,
Stack,
Typography,
Table,
TableCell,
TableBody,
TableHead,
Dialog,
TableRow,
DialogContent,
Tooltip,
Skeleton,
Autocomplete,
TextField,
CircularProgress,
InputBase,
InputAdornment
} from '@mui/material';
import {
MdAccessTime,
MdCancel,
MdCheckCircle,
MdClose,
MdCurrencyRupee,
MdDeleteOutline,
MdHistoryToggleOff,
MdHourglassEmpty,
MdInventory2,
MdLocalShipping,
MdLocationOn,
MdMyLocation,
MdNote,
MdPlace,
MdSearch,
MdStraighten,
MdCalendarMonth,
MdReceiptLong,
MdClear
} from 'react-icons/md';
import TableContainer from '@mui/material/TableContainer';
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 { useInfiniteQuery } from '@tanstack/react-query';
// ============================================================================
// 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 BRAND = '#662582';
const BRAND_LIGHT = '#9255AB';
const SoftPaper = (props) => (
<Paper
{...props}
sx={{
mt: 0.75,
borderRadius: 2,
boxShadow: DT.shadowPop,
border: '1px solid',
borderColor: 'divider',
overflow: 'hidden'
}}
/>
);
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 pillFieldSx = (color) => ({
'& .MuiOutlinedInput-root': {
borderRadius: DT.radiusPill + 'px',
bgcolor: tint(color),
fontWeight: 600,
'& fieldset': { borderColor: edge(color), borderWidth: 1.5 },
'&:hover fieldset': { borderColor: color },
'&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
'&.Mui-focused fieldset': { borderColor: color, borderWidth: 2 }
}
});
// 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' },
{ idx: 1, status: 'pending', label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, countKey: 'pending' },
{ idx: 2, status: 'delivered', label: 'Delivered', color: '#10b981', icon: MdCheckCircle, countKey: 'delivered' },
{ idx: 3, status: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdCancel, countKey: 'cancelled' }
];
// Filled status badge — high-contrast pill (white text on solid color).
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 tid = localStorage.getItem('tenantid');
const tenId = localStorage.getItem('tenantid');
const loadMoreRef = useRef();
const containerRef = useRef();
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [pageCount, setPageCount] = useState(0);
const [percentage1, setPercentage1] = useState('0');
const [percentage2, setPercentage2] = useState('0');
const [percentage3, setPercentage3] = useState('0');
const [percentage4, setPercentage4] = useState('0');
const [tenantLocations, setTenantlocations] = useState([]);
const [coveredorders, setCoveredorders] = useState('');
const [uncoveredorders, setUncoveredorders] = useState('');
const [cancelled, setCancelled] = useState('');
const [created, setCreated] = useState('');
const [loading, setLoading] = useState(false);
const theme = useTheme();
const [tabvalue, setTabvalue] = useState(0);
const [tabstatus, setTabstatus] = useState('Created');
const [currentStatus, setCurrentStatus] = useState('created');
const [createdLenght, setCreatedLenght] = useState();
const [pendingLenght, setPendingLenght] = useState();
const [deliveredlenght, setDeliveredlenght] = useState();
const [cancelledLenght, setCancelledLenght] = useState();
const [cancelOpen, setCancelOpen] = useState(false);
const [orderheaderid, setOrderheaderid] = useState('');
const [locationId, setLocationId] = useState(0);
const [locoName, setLocoName] = useState('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('');
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearch(searchword);
}, 400);
return () => clearTimeout(handler);
}, [searchword]);
const tabCounts = {
created: createdLenght,
pending: pendingLenght,
delivered: deliveredlenght,
cancelled: cancelledLenght
};
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');
const cancelorder = async () => {
setLoading(true);
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();
fetchorderscount();
setCancelOpen(false);
}
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
};
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
});
const rows = rowdata ? rowdata.pages.flatMap((p) => p.data) : [];
useEffect(() => {
if (!hasNextPage) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
fetchNextPage();
}
},
{
// The page (viewport) is now the scroll container, not the table.
// Prefetch the next page ~400px before the sentinel reaches the bottom.
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();
}
}
};
const fetchpercentage = async () => {
setLoading(true);
try {
await axios
.get(`${process.env.REACT_APP_URL}/orders/getordersummary/?tenantid=${tid}`)
.then((res) => {
setCoveredorders(res.data.details.delivered.toString());
setCancelled(res.data.details.cancelled.toString());
setUncoveredorders(res.data.details.pending.toString());
setCreated(res.data.details.created.toString());
setPercentage1((Math.round((res.data.details.created / res.data.details.total) * 100) || 0).toString());
setPercentage3((Math.round((res.data.details.delivered / res.data.details.total) * 100) || 0).toString());
setPercentage4((Math.round((res.data.details.cancelled / res.data.details.total) * 100) || 0).toString());
setPercentage2((Math.round((res.data.details.pending / res.data.details.total) * 100) || 0).toString());
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
} catch (err) {
console.log(err);
setLoading(false);
}
};
useEffect(() => {
fetchpercentage();
}, []);
const fetchorderscount = async () => {
setLoading(true);
try {
await axios
.get(
`${process.env.REACT_APP_URL}/orders/getordersummary/?tenantid=${tid}&locationid=${locationId}&fromdate=${startdate}&todate=${enddate}`
)
.then((res) => {
setCreatedLenght(res.data.details.created);
setPendingLenght(res.data.details.pending);
setDeliveredlenght(res.data.details.delivered);
setCancelledLenght(res.data.details.cancelled);
tabvalue === 0 && setPageCount(res.data.details.created);
tabvalue === 1 && setPageCount(res.data.details.pending);
tabvalue === 2 && setPageCount(res.data.details.delivered);
tabvalue === 3 && setPageCount(res.data.details.cancelled);
setLoading(false);
})
.catch((err) => {
console.log(err);
setLoading(false);
});
} catch (err) {
console.log(err);
setLoading(false);
}
};
useEffect(() => {
fetchorderscount();
}, [tabvalue, locationId, startdate, enddate]);
// ============================================= || gettenantlocations (branches) || =============================================
const gettenantlocations = async (id) => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
setTenantlocations(res.data.details || []);
} catch (err) {
console.log('gettenantlocations', err);
}
};
useEffect(() => {
gettenantlocations(tenId);
}, []);
// KPI tile definitions.
const kpiCards = [
{ key: 'created', label: 'Created Orders', color: BRAND, icon: MdLocalShipping, value: created, percentage: percentage1 },
{ key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdHourglassEmpty, value: uncoveredorders, percentage: percentage2 },
{ key: 'delivered', label: 'Delivered Orders', color: '#10b981', icon: MdCheckCircle, value: coveredorders, percentage: percentage3 },
{ key: 'cancelled', label: 'Cancelled Orders', color: '#ef4444', icon: MdCancel, value: cancelled, percentage: percentage4 }
];
return (
<Fragment>
{loading && (
<>
<Loader />
<CircularLoader />
</>
)}
{/* ============================================= || 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>
{/* Location picker */}
{tenantLocations.length === 1 ? (
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.75,
px: 1.5,
py: 0.75,
borderRadius: 999,
bgcolor: tint(BRAND),
border: `1.5px solid ${edge(BRAND)}`,
color: BRAND,
fontWeight: 800,
fontSize: 13
}}
>
<MdMyLocation size={14} /> {tenantLocations[0].locationname}
</Box>
) : (
<Autocomplete
options={tenantLocations || []}
getOptionLabel={(option) => (option ? `${option.locationname} (${option.suburb || ''})` : '')}
PaperComponent={SoftPaper}
onChange={(event, value, reason) => {
if (value) {
setLocationId(value.locationid);
setLocoName(value.locationname);
}
if (reason === 'clear') {
setLocationId(0);
setLocoName('All Locations');
}
}}
renderInput={(params) => (
<TextField
{...params}
placeholder="Select Location"
size="small"
sx={{ ...pillFieldSx(BRAND), width: { xs: '100%', sm: 260 } }}
InputProps={{
...params.InputProps,
startAdornment: (
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5 }}>
<AccentAvatar color={BRAND} size={22} selected>
<MdMyLocation size={13} />
</AccentAvatar>
</Stack>
)
}}
/>
)}
sx={{ width: { xs: '100%', sm: 280 } }}
/>
)}
</Stack>
</Paper>
{/* ============================================= || KPI Cards (compact) || ============================================= */}
<Grid container spacing={{ xs: 1, sm: 1.25, md: 1.5 }}>
{kpiCards.map((item) => {
const Icon = item.icon;
return (
<Grid item key={item.key} xs={6} sm={6} md={3}>
<Paper
elevation={0}
sx={{
position: 'relative',
overflow: 'hidden',
px: { xs: 1.25, sm: 1.5 },
py: { xs: 0.875, sm: 1.125 },
borderRadius: 2,
border: '1px solid',
borderColor: DT.borderSubtle,
background: '#fff',
transition: 'transform 0.15s, box-shadow 0.15s, border-color 0.15s',
'&:hover': {
transform: 'translateY(-1px)',
boxShadow: DT.shadowMd,
borderColor: edge(item.color)
}
}}
>
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
width: 3,
background: item.color
}}
/>
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1} sx={{ pl: 0.5 }}>
<Stack spacing={0.125} sx={{ minWidth: 0, flex: 1 }}>
<Typography
sx={{
color: DT.textSecondary,
fontWeight: 700,
letterSpacing: 0.4,
textTransform: 'uppercase',
fontSize: 10.5,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
lineHeight: 1.2
}}
>
{item.label}
</Typography>
<Stack direction="row" alignItems="baseline" spacing={0.75}>
<Typography
sx={{
fontWeight: 800,
color: DT.textPrimary,
lineHeight: 1.15,
fontSize: { xs: '0.95rem', sm: '1.1rem', md: '1.2rem' }
}}
noWrap
>
{item.value === '' ? <Skeleton sx={{ width: 40 }} animation="wave" /> : item.value}
</Typography>
{item.percentage != null && item.value !== '' && (
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 700 }}>
{item.percentage}%
</Typography>
)}
</Stack>
</Stack>
<Avatar
variant="rounded"
sx={{
width: 30,
height: 30,
bgcolor: soft(item.color),
color: item.color,
borderRadius: 1.25,
flexShrink: 0
}}
>
<Icon size={15} />
</Avatar>
</Stack>
</Paper>
</Grid>
);
})}
</Grid>
{/* ============================================= || Filter Bar (compact) || ============================================= */}
<Paper
elevation={0}
sx={{
mt: { xs: 1, md: 1.25 },
p: { xs: 1, md: 1.125 },
borderRadius: 2,
border: '1px solid',
borderColor: DT.borderSubtle,
background: '#fff',
boxShadow: DT.shadowSoft
}}
>
<Stack
direction={{ xs: 'column', md: 'row' }}
spacing={1.25}
alignItems={{ xs: 'stretch', md: 'center' }}
justifyContent={{ xs: 'flex-start', md: 'space-between' }}
>
<Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
<Tooltip title="Date Filter" placement="top">
<Box
onClick={() => setDateOpen(true)}
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.75,
px: 1.25,
py: 0.75,
borderRadius: 999,
cursor: 'pointer',
bgcolor: tint('#f59e0b'),
border: `1.5px solid ${edge('#f59e0b')}`,
color: '#f59e0b',
fontWeight: 800,
fontSize: 12,
transition: 'all 0.18s',
'&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` }
}}
>
<MdCalendarMonth size={14} />
{dayjs(startdate).format('DD/MM/YY')} {dayjs(enddate).format('DD/MM/YY')}
</Box>
</Tooltip>
<Box
sx={{
display: 'inline-flex',
alignItems: 'center',
gap: 0.5,
px: 1,
py: 0.5,
borderRadius: 999,
bgcolor: tint(BRAND),
border: `1px solid ${edge(BRAND)}`,
color: BRAND,
fontSize: 11,
fontWeight: 800
}}
>
<MdReceiptLong size={12} /> {datestatus}
</Box>
</Stack>
{tenantLocations.length > 1 && (
<Autocomplete
options={tenantLocations || []}
getOptionLabel={(option) => (option ? `${option.locationname} (${option.suburb || ''})` : '')}
PaperComponent={SoftPaper}
onChange={(event, value, reason) => {
if (value) {
setLocationId(value.locationid);
setLocoName(value.locationname);
}
if (reason === 'clear') {
setLocationId(0);
setLocoName('All Locations');
}
}}
renderInput={(params) => (
<TextField
{...params}
placeholder="Select Location"
size="small"
sx={pillFieldSx('#10b981')}
InputProps={{
...params.InputProps,
startAdornment: (
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ pl: 0.5 }}>
<AccentAvatar color="#10b981" size={22} selected>
<MdPlace size={13} />
</AccentAvatar>
</Stack>
)
}}
/>
)}
sx={{ width: { xs: '100%', md: 320 } }}
/>
)}
</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 = tabCounts[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>
<Box sx={{ width: { xs: '100%', sm: 240, lg: 280 }, flex: { xs: '1 1 100%', sm: '0 0 auto' } }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 0.75,
px: 1.25,
py: 0.5,
borderRadius: 999,
bgcolor: tint(BRAND),
border: `1.5px solid ${edge(BRAND)}`,
transition: 'all 0.18s',
'&:focus-within': {
borderColor: BRAND,
boxShadow: `0 0 0 3px ${ring(BRAND)}`
}
}}
>
<MdSearch size={16} style={{ color: BRAND, flexShrink: 0 }} />
<InputBase
inputRef={textFieldRef}
placeholder="Search (ctrl+k)"
value={searchword}
onChange={(e) => setSearchword(e.target.value)}
autoComplete="off"
sx={{
flex: 1,
fontSize: 13,
fontWeight: 600,
color: DT.textPrimary,
'& input::placeholder': { color: DT.textMuted, opacity: 1 }
}}
/>
{searchword && (
<IconButton
size="small"
onClick={() => {
setSearchword('');
refetchOrders();
fetchorderscount();
}}
sx={{ p: 0.25, color: BRAND }}
>
<MdClear size={14} />
</IconButton>
)}
</Box>
</Box>
</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={{
// Single page scroll: the table is NOT height-capped, so it renders at its
// full height and the whole page scrolls as one. Scrolling down moves the
// KPI cards + header + filter bar off-screen and reveals the full table.
// Only horizontal overflow scrolls inside the container (for wide column sets).
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 || loading) &&
rows.length === 0 &&
Array.from({ length: 10 }).map((_, idx) => (
<TableRow key={`sk-${idx}`}>
{Array.from({ length: currentStatus === 'created' ? 11 : 10 }).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' ? 11 : 10} 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) => (
<TableRow
key={`${row.orderheaderid}-${index}`}
sx={{
cursor: 'pointer',
transition: 'background-color 0.12s, box-shadow 0.12s',
'& td': {
borderBottom: `1px solid ${DT.divider}`,
py: 0.5,
px: 1,
verticalAlign: 'top'
},
'&:hover': {
backgroundColor: tint(BRAND),
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 }}>
<MdNote 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' ? 11 : 10} 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);
}}
/>
</Fragment>
);
};
export default Orders;