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) => ( ); const AccentAvatar = ({ color, selected, size = 24, children }) => ( {children} ); 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 ( {meta.label} ); }; 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 ( {display} ); } return ( {icon} {display} ); }; 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 ( {loading && ( <> )} {/* ============================================= || Header (compact) || ============================================= */} Orders Live · {locoName} · {datestatus} {/* Location picker */} {tenantLocations.length === 1 ? ( {tenantLocations[0].locationname} ) : ( (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) => ( ) }} /> )} sx={{ width: { xs: '100%', sm: 280 } }} /> )} {/* ============================================= || KPI Cards (compact) || ============================================= */} {kpiCards.map((item) => { const Icon = item.icon; return ( {item.label} {item.value === '' ? : item.value} {item.percentage != null && item.value !== '' && ( {item.percentage}% )} ); })} {/* ============================================= || Filter Bar (compact) || ============================================= */} 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')}` } }} > {dayjs(startdate).format('DD/MM/YY')} – {dayjs(enddate).format('DD/MM/YY')} {datestatus} {tenantLocations.length > 1 && ( (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) => ( ) }} /> )} sx={{ width: { xs: '100%', md: 320 } }} /> )} {/* ============================================= || Status Tabs + Search (compact) || ============================================= */} {ORDERS_STATUS_TABS.map((t) => { const Icon = t.icon; const active = tabvalue === t.idx; const count = tabCounts[t.countKey] ?? 0; return ( 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)}` } }} > {t.label} {count ?? 0} ); })} setSearchword(e.target.value)} autoComplete="off" sx={{ flex: 1, fontSize: 13, fontWeight: 600, color: DT.textPrimary, '& input::placeholder': { color: DT.textMuted, opacity: 1 } }} /> {searchword && ( { setSearchword(''); refetchOrders(); fetchorderscount(); }} sx={{ p: 0.25, color: BRAND }} > )} {/* ============================================= || Table (dense, sticky header) || ============================================= */} # Order Location Pickup Drop Qty COD Kms Charges Notes Status {currentStatus === 'created' && Actions} {(isLoadingGetOrders || loading) && rows.length === 0 && Array.from({ length: 10 }).map((_, idx) => ( {Array.from({ length: currentStatus === 'created' ? 11 : 10 }).map((__, ci) => ( ))} ))} {!isLoadingGetOrders && rows.length === 0 && ( No {currentStatus} orders {searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'} {searchword && ( )} )} {rows.map((row, index) => ( {page * rowsPerPage + index + 1} {row.locationname} {row.locationsuburb && ` - ${row.locationsuburb}`} {row.orderid} {(() => { const dateObj = row.pickupslot && dayjs(row.pickupslot).isValid() ? dayjs(row.pickupslot) : dayjs(row.deliverydate || row.orderdate); return ( <> {dateObj.format('hh:mm A')} · {dateObj.format('DD MMM YY')} ); })()} {row.pickupcustomer} {row.pickupcontactno} {row.pickupsuburb || (row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—')} {row.deliverycustomer} {row.deliverycontactno} {row.deliverysuburb || (row.deliveryaddress?.length > 20 ? `${row.deliveryaddress.slice(0, 20)}…` : row.deliveryaddress || '—')} } /> } isMoney /> } /> } isMoney /> {row.ordernotes ? ( {row.ordernotes} ) : ( )} {currentStatus === 'created' && ( {row.orderstatus === 'created' && ( { 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' } }} > )} )} ))} {rows.length !== 0 && ( {isFetchingNextPage || hasNextPage ? ( <> Loading more orders… ) : ( {rows.length} order{rows.length === 1 ? '' : 's'} · End of list )} )}
{/* ============================================= || Cancel Order Dialog || ============================================= */} setCancelOpen(false)} maxWidth="xs" PaperProps={{ sx: { borderRadius: 3 } }}> Cancel Order Are you sure you want to cancel this order? This action cannot be undone. {/* ============================================= || Date Filter || ============================================= */} setDateOpen(false)} onApply={({ startDate, endDate, label }) => { setStartdate(startDate); setEnddate(endDate); setDatestatus(label); }} />
); }; export default Orders;