778 lines
31 KiB
JavaScript
778 lines
31 KiB
JavaScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Spinner } from '@astryxdesign/core/Spinner';
|
|
import { IconButton } from '@astryxdesign/core/IconButton';
|
|
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 { DT, BRAND, BRAND_LIGHT, tint, soft, ring, edge, StatusBadge, AccentAvatar } from '../_shared/ordersDesign';
|
|
import axios from 'axios';
|
|
import dayjs from 'dayjs';
|
|
import logger from 'utils/logger';
|
|
var utc = require('dayjs/plugin/utc');
|
|
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: BRAND, icon: MdLocalShipping },
|
|
{ label: 'Pending', value: 'pending', color: BRAND, icon: MdHourglassEmpty },
|
|
{ label: 'Delivered', value: 'delivered', color: BRAND, icon: MdCheckCircle },
|
|
{ label: 'Cancelled', value: 'cancelled', color: BRAND, icon: MdCancel }
|
|
];
|
|
|
|
// Brand-styled scrollbar reused on the sidebar + table.
|
|
const scrollbarStyle = `
|
|
.rld-scroll::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
.rld-scroll::-webkit-scrollbar-thumb { background-color: ${edge(BRAND)}; border-radius: 8px; }
|
|
.rld-scroll::-webkit-scrollbar-thumb:hover { background-color: ${BRAND}; }
|
|
.rld-scroll::-webkit-scrollbar-track { background-color: ${DT.surfaceAlt}; }
|
|
.rld-pill:focus-within { border-color: ${BRAND} !important; box-shadow: 0 0 0 3px ${ring(BRAND)}; }
|
|
.rld-toggle:hover { background-color: ${tint(BRAND)}; border-color: ${BRAND}; }
|
|
`;
|
|
|
|
const noWrapStyle = { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' };
|
|
|
|
const ResponsiveLocationDrawer = () => {
|
|
const loadMoreRef = useRef();
|
|
const containerRef = useRef();
|
|
const tenantid = localStorage.getItem('tenantid');
|
|
const [isDesktop, setIsDesktop] = useState(() => (typeof window !== 'undefined' ? window.innerWidth >= 900 : true));
|
|
const [open, setOpen] = useState(false);
|
|
const [selectedLocation, setSelectedLocation] = useState(null);
|
|
const [currentStatus, setCurrentStatus] = useState('created');
|
|
const [tabvalue, setTabvalue] = useState(0);
|
|
const [createdLenght, setCreatedLenght] = useState();
|
|
const [pendingLenght, setPendingLenght] = useState();
|
|
const [deliveredlenght, setDeliveredlenght] = useState();
|
|
const [cancelledLenght, setCancelledLenght] = useState();
|
|
const [loading, setLoading] = useState(false);
|
|
const [page, setPage] = React.useState(0);
|
|
const [rowsPerPage, setRowsPerPage] = React.useState(10);
|
|
const [, setPageCount] = React.useState(0);
|
|
const [startdate] = useState(dayjs().format('YYYY-MM-DD'));
|
|
const [enddate] = useState(dayjs().format('YYYY-MM-DD'));
|
|
const [searchLocation, setSearchLocation] = useState('');
|
|
const [debouncedSearchLocation, setDebouncedSearchLocation] = useState('');
|
|
|
|
const [searchword, setSearchword] = useState('');
|
|
const [debouncedSearchword, setDebouncedSearchword] = useState('');
|
|
|
|
useEffect(() => {
|
|
const handleResize = () => setIsDesktop(window.innerWidth >= 900);
|
|
window.addEventListener('resize', handleResize);
|
|
return () => window.removeEventListener('resize', handleResize);
|
|
}, []);
|
|
|
|
// 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);
|
|
}, 400);
|
|
|
|
return () => clearTimeout(handler);
|
|
}, [searchLocation]);
|
|
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => {
|
|
setDebouncedSearchword(searchword);
|
|
}, 400);
|
|
|
|
return () => clearTimeout(handler);
|
|
}, [searchword]);
|
|
|
|
const handleChangetab = (i) => {
|
|
setSearchword('');
|
|
setRowsPerPage(10);
|
|
setTabvalue(i);
|
|
setCurrentStatus(STATUS_TABS[i].value);
|
|
setPage(0);
|
|
};
|
|
|
|
const {
|
|
data: locations,
|
|
isLoading: locationIsLoading,
|
|
isError: locationIsError,
|
|
error: locationError
|
|
} = useQuery({
|
|
queryKey: ['locations', debouncedSearchLocation],
|
|
queryFn: gettenantlocations
|
|
});
|
|
// Auto-pick a sensible default whenever the locations list changes:
|
|
// • Nothing selected yet → pick the first item.
|
|
// • Current selection has been filtered out → also pick the first item
|
|
// (otherwise the orders panel queries a locationid that's no longer
|
|
// 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);
|
|
if (!stillVisible) setSelectedLocation(locations[0]);
|
|
}, [locations]);
|
|
|
|
const {
|
|
data: ordersData,
|
|
fetchNextPage,
|
|
hasNextPage,
|
|
isFetchingNextPage
|
|
} = useInfiniteQuery({
|
|
queryKey: [
|
|
'orders',
|
|
tenantid,
|
|
selectedLocation?.locationid ?? null, // stable
|
|
currentStatus,
|
|
startdate,
|
|
enddate,
|
|
debouncedSearchword,
|
|
rowsPerPage
|
|
],
|
|
queryFn: fetchOrders1,
|
|
getNextPageParam: (lastPage) => lastPage.nextPage
|
|
});
|
|
|
|
const rows = ordersData?.pages?.flatMap((page) => page.details) || [];
|
|
|
|
useEffect(() => {
|
|
if (!hasNextPage) return;
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries[0].isIntersecting) {
|
|
fetchNextPage();
|
|
}
|
|
},
|
|
{
|
|
root: containerRef.current,
|
|
rootMargin: '0px',
|
|
threshold: 1.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 fetchorderscount = async () => {
|
|
setLoading(true);
|
|
try {
|
|
await axios
|
|
.get(
|
|
`${process.env.REACT_APP_URL}/orders/getordersummary/?tenantid=${tenantid}&locationid=${selectedLocation?.locationid}&fromdate=${startdate}&todate=${enddate}`
|
|
)
|
|
.then((res) => {
|
|
logger.info('fetchorderscount', res.data.details);
|
|
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) => {
|
|
logger.error(err);
|
|
setLoading(false);
|
|
});
|
|
} catch (err) {
|
|
logger.error(err);
|
|
setLoading(false);
|
|
}
|
|
};
|
|
useEffect(() => {
|
|
fetchorderscount();
|
|
}, [currentStatus, selectedLocation, startdate, enddate]);
|
|
|
|
useEffect(() => {
|
|
setOpen(isDesktop);
|
|
}, [isDesktop]);
|
|
const toggleDrawer = () => setOpen(!open);
|
|
|
|
const errMessage = locationIsError ? `${locationError.message}` : null;
|
|
|
|
useEffect(() => {
|
|
errMessage && logger.info(errMessage);
|
|
}, [errMessage]);
|
|
|
|
// --------------------------------------------------------------------------
|
|
// Sidebar — searchable location list. Shared between the desktop persistent
|
|
// drawer and the mobile temporary drawer.
|
|
// --------------------------------------------------------------------------
|
|
const sidebarContent = (
|
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', backgroundColor: '#fff' }}>
|
|
{/* Sidebar header */}
|
|
<div style={{ padding: '12px 12px 8px' }}>
|
|
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
width: 30,
|
|
height: 30,
|
|
flexShrink: 0,
|
|
backgroundColor: BRAND,
|
|
color: '#fff',
|
|
borderRadius: 12,
|
|
boxShadow: `0 4px 12px ${ring(BRAND)}`
|
|
}}
|
|
>
|
|
<MdStorefront size={16} />
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
<span style={{ fontSize: 13.5, fontWeight: 800, color: DT.textPrimary, lineHeight: 1.1 }}>Locations</span>
|
|
<span style={{ fontSize: 10.5, fontWeight: 600, color: DT.textMuted }}>
|
|
{Array.isArray(locations) ? `${locations.length} active` : '—'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search pill */}
|
|
<div
|
|
className="rld-pill"
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
padding: '6px 10px',
|
|
borderRadius: 999,
|
|
backgroundColor: tint(BRAND),
|
|
border: `1.5px solid ${edge(BRAND)}`,
|
|
transition: 'all 0.18s'
|
|
}}
|
|
>
|
|
<MdSearch size={16} style={{ color: BRAND, flexShrink: 0 }} />
|
|
<input
|
|
placeholder="Search location"
|
|
value={searchLocation}
|
|
onChange={(e) => setSearchLocation(e.target.value)}
|
|
autoComplete="off"
|
|
style={{ flex: 1, fontSize: 13, fontWeight: 600, color: DT.textPrimary, border: 'none', outline: 'none', background: 'transparent' }}
|
|
/>
|
|
{searchLocation && (
|
|
<IconButton
|
|
label="Clear search"
|
|
icon={<MdClear size={14} />}
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setSearchLocation('')}
|
|
style={{ color: BRAND }}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Location list */}
|
|
<div className="rld-scroll" style={{ flex: 1, overflowY: 'auto', padding: '0 8px 8px' }}>
|
|
{locationIsLoading &&
|
|
Array.from({ length: 8 }).map((_, i) => (
|
|
<div key={i} style={{ padding: '8px' }}>
|
|
<Skeleton avatar active paragraph={{ rows: 1 }} title={false} />
|
|
</div>
|
|
))}
|
|
|
|
{!locationIsLoading && Array.isArray(locations) && locations.length === 0 && (
|
|
<div style={{ padding: '40px 0' }}>
|
|
<Empty description="No locations" />
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
|
{locations?.map((row, index) => {
|
|
const isSelected = row.locationid === selectedLocation?.locationid;
|
|
return (
|
|
<button
|
|
key={index}
|
|
onClick={() => setSelectedLocation(row)}
|
|
style={{
|
|
display: 'flex',
|
|
width: '100%',
|
|
alignItems: 'center',
|
|
justifyContent: 'flex-start',
|
|
textAlign: 'left',
|
|
gap: 8,
|
|
padding: '7px 8px',
|
|
borderRadius: 16,
|
|
border: 'none',
|
|
position: 'relative',
|
|
cursor: 'pointer',
|
|
transition: 'background-color 0.14s, box-shadow 0.14s',
|
|
backgroundColor: isSelected ? tint(BRAND) : 'transparent',
|
|
boxShadow: isSelected ? `inset 3px 0 0 ${BRAND}` : 'none'
|
|
}}
|
|
>
|
|
<AccentAvatar color={BRAND} selected={isSelected} size={36}>
|
|
{row.locationname?.[0]?.toUpperCase() || '?'}
|
|
</AccentAvatar>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 0, minWidth: 0, flex: 1 }}>
|
|
<span style={{ fontSize: 13, fontWeight: 700, color: isSelected ? BRAND : DT.textPrimary, lineHeight: 1.2, ...noWrapStyle }}>
|
|
{row.locationname}
|
|
</span>
|
|
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 3 }}>
|
|
<MdPlace size={11} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
|
<span style={{ fontSize: 11, fontWeight: 600, color: DT.textSecondary, ...noWrapStyle }}>{row.suburb || '—'}</span>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<React.Fragment>
|
|
<style>{scrollbarStyle}</style>
|
|
|
|
{locationIsLoading && (
|
|
<>
|
|
<Loader /> <CircularLoader />
|
|
</>
|
|
)}
|
|
|
|
<div style={{ display: 'flex', width: '100%', height: '100%', position: 'relative', backgroundColor: DT.surfaceAlt }}>
|
|
{/* ---------------- LOCATION SIDEBAR ---------------- */}
|
|
{(isDesktop || open) && (
|
|
<>
|
|
{!isDesktop && (
|
|
<div
|
|
role="button"
|
|
tabIndex={0}
|
|
aria-label="Close locations sidebar"
|
|
onClick={toggleDrawer}
|
|
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && toggleDrawer()}
|
|
style={{ position: 'absolute', inset: 0, backgroundColor: 'rgba(15,23,42,0.35)', zIndex: 9, border: 'none' }}
|
|
/>
|
|
)}
|
|
<div
|
|
style={{
|
|
width: drawerWidth,
|
|
boxSizing: 'border-box',
|
|
position: 'absolute',
|
|
left: 0,
|
|
top: 0,
|
|
height: '100%',
|
|
overflow: 'hidden',
|
|
borderRight: `1px solid ${DT.borderSubtle}`,
|
|
transform: open ? 'translateX(0)' : 'translateX(-100%)',
|
|
transition: 'transform 0.35s ease-in-out',
|
|
zIndex: 10
|
|
}}
|
|
>
|
|
{sidebarContent}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* ---------------- MAIN PANEL ---------------- */}
|
|
<div
|
|
style={{
|
|
flexGrow: 1,
|
|
height: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
overflow: 'hidden',
|
|
paddingLeft: isDesktop && open ? drawerWidth : 0,
|
|
transition: 'padding-left 0.3s ease'
|
|
}}
|
|
>
|
|
{/* ---------------- GRADIENT HEADER ---------------- */}
|
|
<div
|
|
style={{
|
|
flexShrink: 0,
|
|
padding: '10px 14px',
|
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
|
background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
gap: 12,
|
|
flexWrap: 'wrap'
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 10 }}>
|
|
<button
|
|
title={open ? 'Hide locations' : 'Show locations'}
|
|
onClick={toggleDrawer}
|
|
className="rld-toggle"
|
|
style={{
|
|
width: 34,
|
|
height: 34,
|
|
borderRadius: 12,
|
|
backgroundColor: '#fff',
|
|
border: `1px solid ${DT.borderSubtle}`,
|
|
color: BRAND,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
cursor: 'pointer',
|
|
transition: 'background-color 0.15s, border-color 0.15s'
|
|
}}
|
|
>
|
|
<MdMenu size={18} />
|
|
</button>
|
|
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
width: 36,
|
|
height: 36,
|
|
flexShrink: 0,
|
|
backgroundColor: BRAND,
|
|
color: '#fff',
|
|
borderRadius: 12,
|
|
boxShadow: `0 4px 12px ${ring(BRAND)}`
|
|
}}
|
|
>
|
|
<MdMyLocation size={19} />
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
<span
|
|
style={{
|
|
fontWeight: 800,
|
|
color: DT.textPrimary,
|
|
lineHeight: 1.1,
|
|
fontSize: '1.2rem',
|
|
...noWrapStyle
|
|
}}
|
|
>
|
|
{selectedLocation?.locationname || 'Select a location'}
|
|
</span>
|
|
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
|
<span style={{ width: 7, height: 7, borderRadius: '50%', backgroundColor: '#10b981', boxShadow: '0 0 0 3px rgba(16,185,129,0.18)' }} />
|
|
<span style={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 600, ...noWrapStyle }}>
|
|
{selectedLocation?.suburb ? `${selectedLocation.suburb} · ` : ''}Live · {dayjs(startdate).format('DD MMM YYYY')}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Order search pill */}
|
|
<div
|
|
className="rld-pill"
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
padding: '6px 10px',
|
|
borderRadius: 999,
|
|
backgroundColor: '#fff',
|
|
border: `1.5px solid ${edge(BRAND)}`,
|
|
minWidth: 280,
|
|
maxWidth: 360,
|
|
flex: '1 1 280px',
|
|
transition: 'all 0.18s'
|
|
}}
|
|
>
|
|
<MdSearch size={16} style={{ color: BRAND, flexShrink: 0 }} />
|
|
<input
|
|
placeholder="Search order details"
|
|
value={searchword}
|
|
onChange={(e) => setSearchword(e.target.value)}
|
|
autoComplete="off"
|
|
style={{ flex: 1, fontSize: 13, fontWeight: 600, color: DT.textPrimary, border: 'none', outline: 'none', background: 'transparent' }}
|
|
/>
|
|
{searchword && (
|
|
<IconButton
|
|
label="Clear search"
|
|
icon={<MdClear size={14} />}
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setSearchword('')}
|
|
style={{ color: BRAND }}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ---------------- STATUS FILTER PILLS ---------------- */}
|
|
<div
|
|
className="rld-scroll"
|
|
style={{
|
|
flexShrink: 0,
|
|
padding: '8px 12px',
|
|
backgroundColor: '#fff',
|
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
|
display: 'flex',
|
|
gap: 6,
|
|
overflowX: 'auto'
|
|
}}
|
|
>
|
|
{STATUS_TABS.map((item, index) => {
|
|
const isActive = tabvalue === index;
|
|
const Icon = item.icon;
|
|
const count = statusCounts[item.value];
|
|
return (
|
|
<button
|
|
key={index}
|
|
onClick={() => handleChangetab(index)}
|
|
style={{
|
|
flexShrink: 0,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 6,
|
|
padding: '5px 10px',
|
|
borderRadius: 999,
|
|
fontWeight: 700,
|
|
cursor: 'pointer',
|
|
transition: 'all 0.15s',
|
|
border: `1.5px solid ${isActive ? item.color : DT.borderSubtle}`,
|
|
backgroundColor: isActive ? item.color : '#fff',
|
|
color: isActive ? '#fff' : DT.textSecondary,
|
|
boxShadow: isActive ? `0 4px 12px ${ring(item.color)}` : 'none'
|
|
}}
|
|
>
|
|
<Icon size={14} />
|
|
<span style={{ fontSize: 12.5, fontWeight: 700, lineHeight: 1 }}>{item.label}</span>
|
|
<span
|
|
style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
minWidth: 20,
|
|
height: 18,
|
|
padding: '0 5px',
|
|
borderRadius: 999,
|
|
fontSize: 10.5,
|
|
fontWeight: 800,
|
|
backgroundColor: isActive ? 'rgba(255,255,255,0.25)' : soft(item.color),
|
|
color: isActive ? '#fff' : item.color
|
|
}}
|
|
>
|
|
{count ?? 0}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* ---------------- ORDERS TABLE ---------------- */}
|
|
<div style={{ flex: 1, overflow: 'hidden', padding: 12 }}>
|
|
<div
|
|
style={{
|
|
height: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
borderRadius: 8,
|
|
border: `1px solid ${DT.borderSubtle}`,
|
|
overflow: 'hidden',
|
|
background: '#fff',
|
|
boxShadow: DT.shadowSoft
|
|
}}
|
|
>
|
|
<div className="rld-scroll" onScroll={handleScroll} ref={containerRef} style={{ flex: 1, overflow: 'auto' }}>
|
|
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12.5 }}>
|
|
<thead style={{ position: 'sticky', top: 0, zIndex: 1 }}>
|
|
<tr>
|
|
{['#', 'Order', 'Pickup', 'Drop', 'Notes', 'Status'].map((h, i) => (
|
|
<th
|
|
key={h}
|
|
style={{
|
|
textAlign: 'left',
|
|
backgroundColor: DT.surfaceAlt,
|
|
color: DT.textSecondary,
|
|
fontSize: 10.5,
|
|
fontWeight: 800,
|
|
letterSpacing: 0.5,
|
|
textTransform: 'uppercase',
|
|
whiteSpace: 'nowrap',
|
|
borderBottom: `1px solid ${DT.borderSubtle}`,
|
|
padding: '6px 8px',
|
|
width: i === 0 ? 36 : i === 5 ? 120 : undefined,
|
|
minWidth: i > 0 && i < 5 ? 140 : undefined
|
|
}}
|
|
>
|
|
{h}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
{/* LOADING STATE */}
|
|
{loading &&
|
|
Array.from({ length: 10 }).map((_, index) => (
|
|
<tr key={index}>
|
|
{Array.from({ length: 6 }).map((__, i) => (
|
|
<td key={i} style={{ borderBottom: `1px solid ${DT.divider}`, padding: '5px 8px' }}>
|
|
<Skeleton.Input active size="small" style={{ width: '100%', height: 18 }} />
|
|
</td>
|
|
))}
|
|
</tr>
|
|
))}
|
|
|
|
{/* EMPTY STATE */}
|
|
{!loading && rows?.length === 0 && (
|
|
<tr>
|
|
<td colSpan={6} style={{ padding: '56px 0', borderBottom: 'none' }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
width: 56,
|
|
height: 56,
|
|
backgroundColor: soft('#94a3b8'),
|
|
color: DT.textMuted,
|
|
borderRadius: 16
|
|
}}
|
|
>
|
|
<MdReceiptLong size={26} />
|
|
</div>
|
|
<span style={{ fontWeight: 700, color: DT.textPrimary, fontSize: 14 }}>No orders found</span>
|
|
<span style={{ color: DT.textSecondary, fontSize: 12 }}>
|
|
{searchword ? 'Try a different keyword or clear the search.' : 'No orders in this status for the selected location.'}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
|
|
{/* DATA ROWS */}
|
|
{!loading &&
|
|
rows?.map((row, index) => (
|
|
<tr
|
|
key={index}
|
|
className="rld-row"
|
|
style={{ cursor: 'pointer', transition: 'background-color 0.12s, box-shadow 0.12s' }}
|
|
>
|
|
<td style={{ borderBottom: `1px solid ${DT.divider}`, padding: '6px 8px', verticalAlign: 'top' }}>
|
|
<span style={{ fontWeight: 700, fontSize: 12, color: DT.textMuted }}>{page * rowsPerPage + index + 1}</span>
|
|
</td>
|
|
|
|
{/* Order Info */}
|
|
<td style={{ borderBottom: `1px solid ${DT.divider}`, padding: '6px 8px', verticalAlign: 'top' }}>
|
|
<div style={{ fontSize: 12.5, fontWeight: 700, color: DT.textPrimary, lineHeight: 1.25, ...noWrapStyle }}>
|
|
{row.orderid}
|
|
</div>
|
|
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 4, marginTop: 1 }}>
|
|
<MdAccessTime size={10} style={{ color: DT.textMuted, flexShrink: 0 }} />
|
|
<span style={{ fontSize: 10.5, color: DT.textSecondary, fontWeight: 700, ...noWrapStyle }}>
|
|
{dayjs(row.deliverydate).utc().format('hh:mm A')}
|
|
</span>
|
|
<span style={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600, ...noWrapStyle }}>
|
|
· {dayjs(row.deliverydate).utc().format('DD MMM YY')}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
|
|
{/* Pickup */}
|
|
<td style={{ borderBottom: `1px solid ${DT.divider}`, padding: '6px 8px', verticalAlign: 'top' }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
<span style={{ fontSize: 12.5, fontWeight: 700, color: DT.textPrimary, lineHeight: 1.25, ...noWrapStyle }}>
|
|
{row.pickupcustomer || '—'}
|
|
</span>
|
|
<span style={{ fontSize: 11, color: DT.textSecondary, fontWeight: 600, lineHeight: 1.3, ...noWrapStyle }}>
|
|
{row.pickupcontactno}
|
|
</span>
|
|
<span
|
|
title={row.pickupaddress || ''}
|
|
style={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600, lineHeight: 1.3, ...noWrapStyle }}
|
|
>
|
|
{row.pickupsuburb || (row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—')}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
|
|
{/* Drop */}
|
|
<td style={{ borderBottom: `1px solid ${DT.divider}`, padding: '6px 8px', verticalAlign: 'top' }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
|
<span style={{ fontSize: 12.5, fontWeight: 700, color: DT.textPrimary, lineHeight: 1.25, ...noWrapStyle }}>
|
|
{row.deliverycustomer || '—'}
|
|
</span>
|
|
<span style={{ fontSize: 11, color: DT.textSecondary, fontWeight: 600, lineHeight: 1.3, ...noWrapStyle }}>
|
|
{row.deliverycontactno}
|
|
</span>
|
|
<span
|
|
title={row.deliveryaddress || ''}
|
|
style={{ fontSize: 10.5, color: DT.textMuted, fontWeight: 600, lineHeight: 1.3, ...noWrapStyle }}
|
|
>
|
|
{row.deliverysuburb || (row.deliveryaddress ? `${row.deliveryaddress.slice(0, 20)}…` : '—')}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
|
|
{/* Notes */}
|
|
<td style={{ borderBottom: `1px solid ${DT.divider}`, padding: '6px 8px', verticalAlign: 'top' }}>
|
|
<span style={{ fontSize: 11.5, color: DT.textSecondary, fontWeight: 600, lineHeight: 1.35 }}>
|
|
{row.ordernotes || '—'}
|
|
</span>
|
|
</td>
|
|
|
|
{/* Status */}
|
|
<td style={{ borderBottom: `1px solid ${DT.divider}`, padding: '6px 8px', verticalAlign: 'top' }}>
|
|
<StatusBadge status={row.orderstatus} />
|
|
</td>
|
|
</tr>
|
|
))}
|
|
|
|
{rows?.length != 0 && (
|
|
<tr>
|
|
<td colSpan={6} style={{ borderBottom: 'none' }}>
|
|
<div ref={loadMoreRef} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: 40 }}>
|
|
{isFetchingNextPage || hasNextPage ? (
|
|
<Spinner size="md" style={{ color: BRAND }} />
|
|
) : (
|
|
<span style={{ fontSize: 11.5, fontWeight: 700, color: DT.textMuted }}>No more orders</span>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<style>{`.rld-row:hover { background-color: ${tint(BRAND)}; box-shadow: inset 3px 0 0 ${BRAND}; }`}</style>
|
|
</React.Fragment>
|
|
);
|
|
};
|
|
|
|
export default ResponsiveLocationDrawer;
|