updates on the dispatch active page and the navbar design
This commit is contained in:
@@ -1,43 +1,55 @@
|
||||
import React, { useState, useEffect, Fragment, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Drawer,
|
||||
IconButton,
|
||||
Toolbar,
|
||||
Typography,
|
||||
AppBar,
|
||||
useMediaQuery,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
useTheme,
|
||||
ListItemAvatar,
|
||||
Avatar,
|
||||
Tooltip,
|
||||
TableCell,
|
||||
Chip,
|
||||
Stack,
|
||||
TableRow,
|
||||
TableBody,
|
||||
TableHead,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tabs,
|
||||
Tab,
|
||||
CircularProgress
|
||||
CircularProgress,
|
||||
InputBase,
|
||||
Paper,
|
||||
Avatar,
|
||||
ButtonBase
|
||||
} from '@mui/material';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import SearchBar from 'components/nearle_components/SearchBar';
|
||||
import {
|
||||
MdMenu,
|
||||
MdSearch,
|
||||
MdClear,
|
||||
MdPlace,
|
||||
MdStorefront,
|
||||
MdMyLocation,
|
||||
MdAccessTime,
|
||||
MdLocalShipping,
|
||||
MdHourglassEmpty,
|
||||
MdCheckCircle,
|
||||
MdCancel,
|
||||
MdReceiptLong
|
||||
} from 'react-icons/md';
|
||||
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
||||
import { fetchOrders1, gettenantlocations } from '../api/api';
|
||||
import Loader from 'components/Loader';
|
||||
import CircularLoader from 'components/nearle_components/CircularLoader';
|
||||
import { Empty, Skeleton } from 'antd';
|
||||
import MainCard from 'components/MainCard';
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
|
||||
import { CancelOutlined, CheckCircleOutline } from '@mui/icons-material';
|
||||
import {
|
||||
DT,
|
||||
BRAND,
|
||||
BRAND_LIGHT,
|
||||
tint,
|
||||
soft,
|
||||
ring,
|
||||
edge,
|
||||
StatusBadge,
|
||||
AccentAvatar
|
||||
} from '../_shared/ordersDesign';
|
||||
import axios from 'axios';
|
||||
import dayjs from 'dayjs';
|
||||
var utc = require('dayjs/plugin/utc');
|
||||
@@ -45,6 +57,15 @@ dayjs.extend(utc);
|
||||
|
||||
const drawerWidth = 300;
|
||||
|
||||
// Status filter tabs — colors aligned with STATUS_META in the shared design system
|
||||
// (blue=created, amber=pending, green=delivered, red=cancelled).
|
||||
const STATUS_TABS = [
|
||||
{ label: 'Created', value: 'created', color: '#3b82f6', icon: MdLocalShipping },
|
||||
{ label: 'Pending', value: 'pending', color: '#f59e0b', icon: MdHourglassEmpty },
|
||||
{ label: 'Delivered', value: 'delivered', color: '#10b981', icon: MdCheckCircle },
|
||||
{ label: 'Cancelled', value: 'cancelled', color: '#ef4444', icon: MdCancel }
|
||||
];
|
||||
|
||||
const ResponsiveLocationDrawer = () => {
|
||||
const loadMoreRef = useRef();
|
||||
const containerRef = useRef();
|
||||
@@ -71,6 +92,14 @@ const ResponsiveLocationDrawer = () => {
|
||||
const [searchword, setSearchword] = useState('');
|
||||
const [debouncedSearchword, setDebouncedSearchword] = useState('');
|
||||
|
||||
// Per-status counts keyed by tab value, so the filter pills can show a badge.
|
||||
const statusCounts = {
|
||||
created: createdLenght,
|
||||
pending: pendingLenght,
|
||||
delivered: deliveredlenght,
|
||||
cancelled: cancelledLenght
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
setDebouncedSearchLocation(searchLocation);
|
||||
@@ -87,38 +116,11 @@ const ResponsiveLocationDrawer = () => {
|
||||
return () => clearTimeout(handler);
|
||||
}, [searchword]);
|
||||
|
||||
const statusMap = [
|
||||
{
|
||||
label: 'Created',
|
||||
value: 'created',
|
||||
count: createdLenght,
|
||||
icon: <AccessTimeIcon color="primary" fontSize="small" />
|
||||
},
|
||||
{
|
||||
label: 'Pending',
|
||||
value: 'pending',
|
||||
count: pendingLenght,
|
||||
icon: <LocalShippingOutlinedIcon color="primary" fontSize="small" />
|
||||
},
|
||||
{
|
||||
label: 'Delivered',
|
||||
value: 'delivered',
|
||||
count: deliveredlenght,
|
||||
icon: <CheckCircleOutline color="primary" fontSize="small" />
|
||||
},
|
||||
{
|
||||
label: 'Cancelled',
|
||||
value: 'cancelled',
|
||||
count: cancelledLenght,
|
||||
icon: <CancelOutlined color="primary" fontSize="small" />
|
||||
}
|
||||
];
|
||||
|
||||
const handleChangetab = (e, i) => {
|
||||
const handleChangetab = (i) => {
|
||||
setSearchword('');
|
||||
setRowsPerPage(10);
|
||||
setTabvalue(i);
|
||||
setCurrentStatus(statusMap[i].value);
|
||||
setCurrentStatus(STATUS_TABS[i].value);
|
||||
setPage(0);
|
||||
};
|
||||
|
||||
@@ -138,9 +140,7 @@ const ResponsiveLocationDrawer = () => {
|
||||
// in the visible list, returning nothing and confusing the operator).
|
||||
useEffect(() => {
|
||||
if (!Array.isArray(locations) || locations.length === 0) return;
|
||||
const stillVisible =
|
||||
selectedLocation &&
|
||||
locations.some((l) => l.locationid === selectedLocation.locationid);
|
||||
const stillVisible = selectedLocation && locations.some((l) => l.locationid === selectedLocation.locationid);
|
||||
if (!stillVisible) setSelectedLocation(locations[0]);
|
||||
}, [locations]);
|
||||
|
||||
@@ -177,7 +177,7 @@ const ResponsiveLocationDrawer = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
root: document.querySelector('.MuiTableContainer-root'), // 👈 or explicitly TableContainer
|
||||
root: document.querySelector('.MuiTableContainer-root'),
|
||||
rootMargin: '0px',
|
||||
threshold: 1.0
|
||||
}
|
||||
@@ -240,6 +240,139 @@ const ResponsiveLocationDrawer = () => {
|
||||
errMessage && console.log(errMessage);
|
||||
}, [errMessage]);
|
||||
|
||||
// Brand-styled scrollbar reused on the sidebar + table.
|
||||
const scrollbarSx = {
|
||||
'&::-webkit-scrollbar': { width: 8, height: 8 },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: edge(BRAND),
|
||||
borderRadius: 8,
|
||||
'&:hover': { backgroundColor: BRAND }
|
||||
},
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: DT.surfaceAlt }
|
||||
};
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Sidebar — searchable location list. Shared between the desktop persistent
|
||||
// drawer and the mobile temporary drawer.
|
||||
// --------------------------------------------------------------------------
|
||||
const sidebarContent = (
|
||||
<Stack sx={{ height: '100%', bgcolor: '#fff' }}>
|
||||
{/* Sidebar header */}
|
||||
<Box sx={{ px: 1.5, pt: 1.5, pb: 1 }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1.25 }}>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
sx={{ width: 30, height: 30, bgcolor: BRAND, color: '#fff', borderRadius: 1.5, boxShadow: `0 4px 12px ${ring(BRAND)}` }}
|
||||
>
|
||||
<MdStorefront size={16} />
|
||||
</Avatar>
|
||||
<Stack spacing={0}>
|
||||
<Typography sx={{ fontSize: 13.5, fontWeight: 800, color: DT.textPrimary, lineHeight: 1.1 }}>Locations</Typography>
|
||||
<Typography sx={{ fontSize: 10.5, fontWeight: 600, color: DT.textMuted }}>
|
||||
{Array.isArray(locations) ? `${locations.length} active` : '—'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Search pill */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
px: 1.25,
|
||||
py: 0.75,
|
||||
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
|
||||
placeholder="Search location"
|
||||
value={searchLocation}
|
||||
onChange={(e) => setSearchLocation(e.target.value)}
|
||||
autoComplete="off"
|
||||
sx={{
|
||||
flex: 1,
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
color: DT.textPrimary,
|
||||
'& input::placeholder': { color: DT.textMuted, opacity: 1 }
|
||||
}}
|
||||
/>
|
||||
{searchLocation && (
|
||||
<IconButton size="small" onClick={() => setSearchLocation('')} sx={{ p: 0.25, color: BRAND }}>
|
||||
<MdClear size={14} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Location list */}
|
||||
<Box sx={{ flex: 1, overflowY: 'auto', px: 1, pb: 1, ...scrollbarSx }}>
|
||||
{locationIsLoading &&
|
||||
Array.from({ length: 8 }).map((_, i) => (
|
||||
<Box key={i} sx={{ px: 1, py: 1 }}>
|
||||
<Skeleton avatar active paragraph={{ rows: 1 }} title={false} />
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{!locationIsLoading && Array.isArray(locations) && locations.length === 0 && (
|
||||
<Box sx={{ py: 5 }}>
|
||||
<Empty description="No locations" />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Stack spacing={0.5}>
|
||||
{locations?.map((row, index) => {
|
||||
const isSelected = row.locationid === selectedLocation?.locationid;
|
||||
return (
|
||||
<ButtonBase
|
||||
key={index}
|
||||
onClick={() => setSelectedLocation(row)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
justifyContent: 'flex-start',
|
||||
textAlign: 'left',
|
||||
gap: 1,
|
||||
px: 1,
|
||||
py: 0.875,
|
||||
borderRadius: 2,
|
||||
position: 'relative',
|
||||
transition: 'background-color 0.14s, box-shadow 0.14s',
|
||||
bgcolor: isSelected ? tint(BRAND) : 'transparent',
|
||||
boxShadow: isSelected ? `inset 3px 0 0 ${BRAND}` : 'none',
|
||||
'&:hover': { bgcolor: isSelected ? tint(BRAND) : DT.surfaceAlt }
|
||||
}}
|
||||
>
|
||||
<AccentAvatar color={BRAND} selected={isSelected} size={36}>
|
||||
{row.locationname?.[0]?.toUpperCase() || '?'}
|
||||
</AccentAvatar>
|
||||
<Stack spacing={0} sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Typography
|
||||
sx={{ fontSize: 13, fontWeight: 700, color: isSelected ? BRAND : DT.textPrimary, lineHeight: 1.2 }}
|
||||
noWrap
|
||||
>
|
||||
{row.locationname}
|
||||
</Typography>
|
||||
<Stack direction="row" alignItems="center" spacing={0.375}>
|
||||
<MdPlace size={11} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
||||
<Typography sx={{ fontSize: 11, fontWeight: 600, color: DT.textSecondary }} noWrap>
|
||||
{row.suburb || '—'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</ButtonBase>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{locationIsLoading && (
|
||||
@@ -248,9 +381,8 @@ const ResponsiveLocationDrawer = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: 'flex', width: '100%', height: '100%', position: 'relative' }}>
|
||||
{/* ---------------- LOCAL DRAWER ---------------- */}
|
||||
|
||||
<Box sx={{ display: 'flex', width: '100%', height: '100%', position: 'relative', bgcolor: DT.surfaceAlt }}>
|
||||
{/* ---------------- LOCATION SIDEBAR ---------------- */}
|
||||
<Drawer
|
||||
variant={isDesktop ? 'persistent' : 'temporary'}
|
||||
open={open}
|
||||
@@ -264,246 +396,252 @@ const ResponsiveLocationDrawer = () => {
|
||||
left: 0,
|
||||
top: 0,
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
overflow: 'hidden',
|
||||
borderRight: `1px solid ${DT.borderSubtle}`,
|
||||
transition: 'transform 0.35s ease-in-out',
|
||||
zIndex: 10,
|
||||
|
||||
/* vertical scrollbar */
|
||||
'&::-webkit-scrollbar:vertical': {
|
||||
width: '7px',
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.3s'
|
||||
},
|
||||
|
||||
/* horizontal scrollbar */
|
||||
'&::-webkit-scrollbar:horizontal': {
|
||||
height: '6px', // thinner horizontal bar
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.3s'
|
||||
},
|
||||
|
||||
/* show scrollbar when hovering drawer */
|
||||
'&:hover::-webkit-scrollbar': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
/* thumb styling */
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: '8px'
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
backgroundColor: theme.palette.primary.dark
|
||||
},
|
||||
|
||||
/* track styling */
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: theme.palette.primary.lighter
|
||||
}
|
||||
zIndex: 10
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'sticky', top: 0, zIndex: 11, border: 'none' }}>
|
||||
<SearchBar
|
||||
value={searchLocation}
|
||||
placeholder="Search Location"
|
||||
onChange={(e) => setSearchLocation(e.target.value)}
|
||||
sx={{
|
||||
width: 'auto',
|
||||
height: 60,
|
||||
bgcolor: 'white',
|
||||
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
border: 'none',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: theme.palette.secondary.light
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<List sx={{ border: 'none', mt: -1 }}>
|
||||
{locations?.map((row, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<ListItem
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
bgcolor: row.locationid == selectedLocation?.locationid ? theme.palette.secondary[200] : 'none',
|
||||
'&:hover': {
|
||||
bgcolor: theme.palette.secondary.lighter
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedLocation(row);
|
||||
}}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
bgcolor: 'primary.main', // background color
|
||||
color: 'white' // text color
|
||||
}}
|
||||
>
|
||||
{row.locationname?.[0]?.toUpperCase() || '?'}
|
||||
</Avatar>{' '}
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={row.locationname} secondary={row.suburb} />
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</React.Fragment>
|
||||
))}
|
||||
</List>
|
||||
{sidebarContent}
|
||||
</Drawer>
|
||||
{/* -------------- LOCAL PAGE APPBAR -------------- */}
|
||||
<AppBar
|
||||
elevation={0}
|
||||
position="absolute"
|
||||
sx={{
|
||||
top: 0,
|
||||
left: open && isDesktop ? `${drawerWidth}px` : 0,
|
||||
width: open && isDesktop ? `calc(100% - ${drawerWidth}px)` : '100%',
|
||||
transition: 'left 0.3s ease, width 0.3s ease',
|
||||
zIndex: 1100, // BELOW drawer, ABOVE content
|
||||
backgroundColor: 'white',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: theme.palette.secondary.light
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<Stack
|
||||
sx={{ width: '100%', borderBottom: '1px soild red' }}
|
||||
display={'flex'}
|
||||
flexDirection={'row'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
flexWrap={'wrap'}
|
||||
>
|
||||
<Stack display={'flex'} flexDirection={'row'} alignItems={'center'}>
|
||||
<IconButton color="primary" onClick={toggleDrawer} sx={{ mr: 1 }}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
|
||||
<Typography variant="h5" color={'primary'} sx={{ whiteSpace: 'nowrap', ml: 2 }}>
|
||||
{selectedLocation?.locationname}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<SearchBar
|
||||
value={searchword}
|
||||
placeholder={'Search Order Details'}
|
||||
onChange={(e) => setSearchword(e.target.value)}
|
||||
sx={{
|
||||
width: 'auto',
|
||||
height: 40,
|
||||
bgcolor: 'white',
|
||||
maxWidth: 800,
|
||||
borderRadius: 1
|
||||
// '& .MuiOutlinedInput-notchedOutline': {
|
||||
// border: 'none'
|
||||
// }
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
{/* ---------------- PAGE SCROLLABLE CONTENT ---------------- */}
|
||||
{/* ---------------- MAIN PANEL ---------------- */}
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
pt: '64px', // Height of AppBar
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
pl: isDesktop && open ? `${drawerWidth}px` : 0,
|
||||
transition: 'padding-left 0.3s ease',
|
||||
mt: -1
|
||||
transition: 'padding-left 0.3s ease'
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
display={'flex'}
|
||||
flexDirection={'row'}
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
flexWrap={'wrap-reverse'}
|
||||
gap={2}
|
||||
{/* ---------------- GRADIENT HEADER ---------------- */}
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
border: '1px solid ',
|
||||
borderBottom: 0,
|
||||
borderColor: 'bg.main',
|
||||
p: 1.5
|
||||
flexShrink: 0,
|
||||
px: { xs: 1.25, sm: 1.75 },
|
||||
py: { xs: 1, sm: 1.25 },
|
||||
borderRadius: 0,
|
||||
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||||
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`
|
||||
}}
|
||||
>
|
||||
{/* Tabs Wrapper */}
|
||||
<Stack
|
||||
direction={{ xs: 'column', md: 'row' }}
|
||||
alignItems={{ xs: 'stretch', md: 'center' }}
|
||||
justifyContent="space-between"
|
||||
spacing={{ xs: 1, md: 1.5 }}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1.25}>
|
||||
<Tooltip title={open ? 'Hide locations' : 'Show locations'} arrow>
|
||||
<IconButton
|
||||
onClick={toggleDrawer}
|
||||
sx={{
|
||||
width: 34,
|
||||
height: 34,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: '#fff',
|
||||
border: `1px solid ${DT.borderSubtle}`,
|
||||
color: BRAND,
|
||||
'&:hover': { bgcolor: tint(BRAND), borderColor: BRAND }
|
||||
}}
|
||||
>
|
||||
<MdMenu size={18} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tabs value={tabvalue} onChange={handleChangetab} variant="scrollable" scrollButtons="auto" allowScrollButtonsMobile>
|
||||
{statusMap.map((item, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
label={
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
<Chip label={item.count} color="primary" variant="light" size="small" />
|
||||
</Stack>
|
||||
}
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
sx={{ width: 36, height: 36, bgcolor: BRAND, color: '#fff', borderRadius: 1.5, boxShadow: `0 4px 12px ${ring(BRAND)}` }}
|
||||
>
|
||||
<MdMyLocation size={19} />
|
||||
</Avatar>
|
||||
<Stack spacing={0.125}>
|
||||
<Typography
|
||||
variant="h3"
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
color: DT.textPrimary,
|
||||
lineHeight: 1.1,
|
||||
fontSize: { xs: '1.05rem', sm: '1.2rem', md: '1.3rem' }
|
||||
}}
|
||||
noWrap
|
||||
>
|
||||
{selectedLocation?.locationname || 'Select a location'}
|
||||
</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 }} noWrap>
|
||||
{selectedLocation?.suburb ? `${selectedLocation.suburb} · ` : ''}Live · {dayjs(startdate).format('DD MMM YYYY')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
{/* Order search pill */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.75,
|
||||
px: 1.25,
|
||||
py: 0.75,
|
||||
borderRadius: 999,
|
||||
bgcolor: '#fff',
|
||||
border: `1.5px solid ${edge(BRAND)}`,
|
||||
minWidth: { xs: '100%', md: 280 },
|
||||
maxWidth: { md: 360 },
|
||||
transition: 'all 0.18s',
|
||||
'&:focus-within': { borderColor: BRAND, boxShadow: `0 0 0 3px ${ring(BRAND)}` }
|
||||
}}
|
||||
>
|
||||
<MdSearch size={16} style={{ color: BRAND, flexShrink: 0 }} />
|
||||
<InputBase
|
||||
placeholder="Search order details"
|
||||
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 }
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Tabs>
|
||||
</Stack>
|
||||
<MainCard
|
||||
content={false}
|
||||
{searchword && (
|
||||
<IconButton size="small" onClick={() => setSearchword('')} sx={{ p: 0.25, color: BRAND }}>
|
||||
<MdClear size={14} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* ---------------- STATUS FILTER PILLS ---------------- */}
|
||||
<Box
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
height: 'calc(100vh - 200px)', // adjust as needed
|
||||
flexShrink: 0,
|
||||
px: { xs: 1, sm: 1.5 },
|
||||
py: 1,
|
||||
bgcolor: '#fff',
|
||||
borderBottom: `1px solid ${DT.borderSubtle}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
gap: 0.75,
|
||||
overflowX: 'auto',
|
||||
...scrollbarSx
|
||||
}}
|
||||
>
|
||||
<Fragment>
|
||||
{/* Scrollable table container */}
|
||||
{STATUS_TABS.map((item, index) => {
|
||||
const isActive = tabvalue === index;
|
||||
const Icon = item.icon;
|
||||
const count = statusCounts[item.value];
|
||||
return (
|
||||
<ButtonBase
|
||||
key={index}
|
||||
onClick={() => handleChangetab(index)}
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
gap: 0.75,
|
||||
px: 1.25,
|
||||
py: 0.625,
|
||||
borderRadius: 999,
|
||||
fontWeight: 700,
|
||||
transition: 'all 0.15s',
|
||||
border: `1.5px solid ${isActive ? item.color : DT.borderSubtle}`,
|
||||
bgcolor: isActive ? item.color : '#fff',
|
||||
color: isActive ? '#fff' : DT.textSecondary,
|
||||
boxShadow: isActive ? `0 4px 12px ${ring(item.color)}` : 'none',
|
||||
'&:hover': {
|
||||
borderColor: item.color,
|
||||
color: isActive ? '#fff' : item.color,
|
||||
bgcolor: isActive ? item.color : tint(item.color)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon size={14} />
|
||||
<Typography sx={{ fontSize: 12.5, fontWeight: 700, lineHeight: 1 }}>{item.label}</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minWidth: 20,
|
||||
height: 18,
|
||||
px: 0.625,
|
||||
borderRadius: 999,
|
||||
fontSize: 10.5,
|
||||
fontWeight: 800,
|
||||
bgcolor: isActive ? 'rgba(255,255,255,0.25)' : soft(item.color),
|
||||
color: isActive ? '#fff' : item.color
|
||||
}}
|
||||
>
|
||||
{count ?? 0}
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
|
||||
{/* ---------------- ORDERS TABLE ---------------- */}
|
||||
<Box sx={{ flex: 1, overflow: 'hidden', p: { xs: 1, sm: 1.5 } }}>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: 2,
|
||||
border: `1px solid ${DT.borderSubtle}`,
|
||||
overflow: 'hidden',
|
||||
background: '#fff',
|
||||
boxShadow: DT.shadowSoft
|
||||
}}
|
||||
>
|
||||
<TableContainer
|
||||
onScroll={handleScroll}
|
||||
ref={containerRef}
|
||||
sx={{
|
||||
width: '100%',
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
borderBottom: 1,
|
||||
maxHeight: 'calc(100vh - 225px)',
|
||||
borderColor: 'divider',
|
||||
'&::-webkit-scrollbar': { width: '12px' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: '8px'
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
backgroundColor: theme.palette.primary.dark
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: theme.palette.primary.lighter
|
||||
}
|
||||
}}
|
||||
sx={{ flex: 1, overflow: 'auto', ...scrollbarSx }}
|
||||
>
|
||||
<Table stickyHeader>
|
||||
{/* HEADER */}
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ backgroundColor: theme.palette.secondary.light, position: 'sticky !important' }}>S.No</TableCell>
|
||||
<TableCell sx={{ backgroundColor: theme.palette.secondary.light, position: 'sticky !important' }}>Orders</TableCell>
|
||||
<TableCell sx={{ backgroundColor: theme.palette.secondary.light, position: 'sticky !important' }}>Pickup</TableCell>
|
||||
<TableCell sx={{ backgroundColor: theme.palette.secondary.light, position: 'sticky !important' }}>Drop</TableCell>
|
||||
<TableCell sx={{ backgroundColor: theme.palette.secondary.light, position: 'sticky !important' }}>Notes</TableCell>
|
||||
<TableCell sx={{ backgroundColor: theme.palette.secondary.light, position: 'sticky !important' }}>Status</TableCell>
|
||||
<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 sx={{ width: 36 }}>#</TableCell>
|
||||
<TableCell sx={{ minWidth: 150 }}>Order</TableCell>
|
||||
<TableCell sx={{ minWidth: 150 }}>Pickup</TableCell>
|
||||
<TableCell sx={{ minWidth: 150 }}>Drop</TableCell>
|
||||
<TableCell sx={{ minWidth: 140 }}>Notes</TableCell>
|
||||
<TableCell sx={{ width: 120 }}>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
||||
{/* BODY */}
|
||||
<TableBody>
|
||||
{/* LOADING STATE */}
|
||||
{loading &&
|
||||
[...Array(10)].map((_, index) => (
|
||||
Array.from({ length: 10 }).map((_, index) => (
|
||||
<TableRow key={index}>
|
||||
{[...Array(6)].map((__, i) => (
|
||||
<TableCell key={i}>
|
||||
<Skeleton animation="wave" />
|
||||
{Array.from({ length: 6 }).map((__, i) => (
|
||||
<TableCell key={i} sx={{ borderBottom: `1px solid ${DT.divider}`, py: 0.625, px: 1 }}>
|
||||
<Skeleton.Input active size="small" style={{ width: '100%', height: 18 }} />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
@@ -512,8 +650,16 @@ const ResponsiveLocationDrawer = () => {
|
||||
{/* EMPTY STATE */}
|
||||
{!loading && rows?.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} sx={{ minWidth: '100%', height: 500 }} align="center">
|
||||
<Empty description={'No Orders'} />
|
||||
<TableCell colSpan={6} 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 }}>
|
||||
<MdReceiptLong size={26} />
|
||||
</Avatar>
|
||||
<Typography sx={{ fontWeight: 700, color: DT.textPrimary, fontSize: 14 }}>No orders found</Typography>
|
||||
<Typography sx={{ color: DT.textSecondary, fontSize: 12 }}>
|
||||
{searchword ? 'Try a different keyword or clear the search.' : 'No orders in this status for the selected location.'}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
@@ -521,83 +667,101 @@ const ResponsiveLocationDrawer = () => {
|
||||
{/* DATA ROWS */}
|
||||
{!loading &&
|
||||
rows?.map((row, index) => (
|
||||
<TableRow key={index} sx={{ cursor: 'pointer' }}>
|
||||
<TableCell>{page * rowsPerPage + index + 1}</TableCell>
|
||||
<TableRow
|
||||
key={index}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
transition: 'background-color 0.12s, box-shadow 0.12s',
|
||||
'& td': { borderBottom: `1px solid ${DT.divider}`, py: 0.75, px: 1, verticalAlign: 'top' },
|
||||
'&:hover': { backgroundColor: tint(BRAND), boxShadow: `inset 3px 0 0 ${BRAND}` }
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<Typography sx={{ fontWeight: 700, fontSize: 12, color: DT.textMuted }}>{page * rowsPerPage + index + 1}</Typography>
|
||||
</TableCell>
|
||||
|
||||
{/* Order Info */}
|
||||
<TableCell>
|
||||
<Typography variant="body2" noWrap>
|
||||
<Typography sx={{ fontSize: 12.5, fontWeight: 700, color: DT.textPrimary, lineHeight: 1.25 }} noWrap>
|
||||
{row.orderid}
|
||||
</Typography>
|
||||
<Typography variant="caption" noWrap>
|
||||
{dayjs(row.deliverydate).utc().format('DD/MM/YYYY')}
|
||||
</Typography>
|
||||
<Typography variant="caption" noWrap>
|
||||
{dayjs(row.deliverydate).utc().format('hh:mm A')}
|
||||
</Typography>
|
||||
<Stack direction="row" alignItems="center" spacing={0.5} sx={{ mt: 0.125 }}>
|
||||
<MdAccessTime size={10} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
||||
<Typography sx={{ fontSize: 10.5, color: DT.textSecondary, fontWeight: 700 }} noWrap>
|
||||
{dayjs(row.deliverydate).utc().format('hh:mm A')}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600 }} noWrap>
|
||||
· {dayjs(row.deliverydate).utc().format('DD MMM YY')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
|
||||
{/* Pickup */}
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Avatar sx={{ width: 25, height: 25 }} />
|
||||
<Stack>
|
||||
<Typography variant="caption">{row.pickupcustomer}</Typography>
|
||||
<Typography variant="caption">{row.pickupcontactno}</Typography>
|
||||
<Tooltip title={row.pickupaddress || ''}>
|
||||
<Typography variant="caption">{row.pickupsuburb || row.pickupaddress?.slice(0, 20) || '—'}</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Stack spacing={0.125}>
|
||||
<Typography sx={{ fontSize: 12.5, fontWeight: 700, color: DT.textPrimary, lineHeight: 1.25 }} noWrap>
|
||||
{row.pickupcustomer || '—'}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: 11, color: DT.textSecondary, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||||
{row.pickupcontactno}
|
||||
</Typography>
|
||||
<Tooltip title={row.pickupaddress || ''}>
|
||||
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||||
{row.pickupsuburb || (row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—')}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
|
||||
{/* Drop */}
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Avatar sx={{ width: 25, height: 25 }} />
|
||||
<Stack>
|
||||
<Typography variant="caption">{row.deliverycustomer}</Typography>
|
||||
<Typography variant="caption">{row.deliverycontactno}</Typography>
|
||||
<Tooltip title={row.deliveryaddress || ''}>
|
||||
<Typography variant="caption">{row.deliverysuburb || row.deliveryaddress?.slice(0, 20) || '—'}</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Stack spacing={0.125}>
|
||||
<Typography sx={{ fontSize: 12.5, fontWeight: 700, color: DT.textPrimary, lineHeight: 1.25 }} noWrap>
|
||||
{row.deliverycustomer || '—'}
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: 11, color: DT.textSecondary, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||||
{row.deliverycontactno}
|
||||
</Typography>
|
||||
<Tooltip title={row.deliveryaddress || ''}>
|
||||
<Typography sx={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600, lineHeight: 1.3 }} noWrap>
|
||||
{row.deliverysuburb || (row.deliveryaddress ? `${row.deliveryaddress.slice(0, 20)}…` : '—')}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
|
||||
{/* Notes */}
|
||||
<TableCell>{row.ordernotes}</TableCell>
|
||||
<TableCell>
|
||||
<Typography sx={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 600, lineHeight: 1.35 }}>
|
||||
{row.ordernotes || '—'}
|
||||
</Typography>
|
||||
</TableCell>
|
||||
|
||||
{/* Status */}
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1}>
|
||||
{row.orderstatus === 'pending' && <Chip label="Pending" color="warning" size="small" />}
|
||||
{row.orderstatus === 'confirmed' && <Chip label="Confirmed" color="success" size="small" />}
|
||||
{row.orderstatus === 'cancelled' && <Chip label="Cancelled" color="error" size="small" />}
|
||||
{row.orderstatus === 'delivered' && <Chip label="Completed" color="primary" size="small" />}
|
||||
{row.orderstatus === 'processing' && <Chip label="Processing" color="primary" size="small" />}
|
||||
{row.orderstatus === 'ready' && <Chip label="Accepted" color="info" size="small" />}
|
||||
{row.orderstatus === 'active' && <Chip label="Picked" color="info" size="small" />}
|
||||
{row.orderstatus === 'closed' && <Chip label="Closed" color="info" size="small" />}
|
||||
{row.orderstatus === 'created' && <Chip label="Created" color="secondary" size="small" />}
|
||||
</Stack>
|
||||
<StatusBadge status={row.orderstatus} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
{rows?.length != 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} rowSpan={3}>
|
||||
<div ref={loadMoreRef} style={{ height: 40, textAlign: 'center' }}>
|
||||
{isFetchingNextPage ? <CircularProgress /> : hasNextPage ? <CircularProgress /> : 'No More Orders'}
|
||||
</div>
|
||||
<TableCell colSpan={6} sx={{ borderBottom: 'none' }}>
|
||||
<Stack ref={loadMoreRef} alignItems="center" justifyContent="center" sx={{ height: 40 }}>
|
||||
{isFetchingNextPage || hasNextPage ? (
|
||||
<CircularProgress size={20} sx={{ color: BRAND }} />
|
||||
) : (
|
||||
<Typography sx={{ fontSize: 11.5, fontWeight: 700, color: DT.textMuted }}>No more orders</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Fragment>
|
||||
</MainCard>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
|
||||
Reference in New Issue
Block a user