import * as React from 'react'; import { enqueueSnackbar } from 'notistack'; import { useState, useEffect, Fragment, useRef } from 'react'; import dayjs from 'dayjs'; var utc = require('dayjs/plugin/utc'); dayjs.extend(utc); import axios from 'axios'; import { Avatar, Box, Button, Grid, IconButton, Paper, Stack, Typography, Table, TableCell, TableBody, TableHead, Dialog, TableRow, DialogContent, Tooltip, Skeleton, CircularProgress, InputBase, Backdrop, SpeedDial, SpeedDialIcon, SpeedDialAction, Badge, TableContainer, Checkbox } from '@mui/material'; import { MdAccessTime, MdCancel, MdCheckCircle, MdClose, MdCurrencyRupee, MdDeleteOutline, MdHistoryToggleOff, MdHourglassEmpty, MdInventory2, MdLocalShipping, MdSearch, MdStraighten, MdCalendarMonth, MdReceiptLong, MdClear, MdNotes } from 'react-icons/md'; import { DeleteOutlined } from '@ant-design/icons'; import Loader from 'components/Loader'; import { useHotkeyFocus } from 'components/nearle_components/useHotkeyFocus'; import DateFilterDialog from 'components/nearle_components/DateFilterDialog'; import CircularLoader from 'components/nearle_components/CircularLoader'; import AiImage from '../../../assets/images/aiImage.png'; import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query'; import { useNavigate, useLocation } from 'react-router-dom'; import { fetchPercentageData, createAutomationDeliveries, cancelMultipleOrder, getallriders, fetchorderscount } from '../api/api'; // ============================================================================ // Design tokens — shared with the rest of the redesigned operator pages. // ============================================================================ const DT = { radiusPill: 999, radiusCard: 16, shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)', shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)', shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)', textPrimary: '#0f172a', textSecondary: '#64748b', textMuted: '#94a3b8', borderSubtle: '#e2e8f0', divider: '#f1f5f9', surface: '#ffffff', surfaceAlt: '#f8fafc' }; const dtA = (c, suffix) => `${c}${suffix}`; const tint = (c) => dtA(c, '08'); const soft = (c) => dtA(c, '18'); const ring = (c) => dtA(c, '26'); const edge = (c) => dtA(c, '55'); const dtTint = tint; const dtSoft = soft; const dtRing = ring; const dtEdge = edge; const BRAND = '#662582'; const BRAND_LIGHT = '#9255AB'; // Semantic per-row status palette — colors per brand standard: // green=delivered, amber=pending, blue=created/processing, red=cancelled, // dark-red=failed, purple=on-hold. const ROW_STATUS_META = { created: { label: 'Created', color: '#3b82f6', icon: MdLocalShipping }, pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, processing: { label: 'Processing', color: '#3b82f6', icon: MdAccessTime }, modified: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle }, confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle }, ready: { label: 'Accepted', color: '#6366f1', icon: MdCheckCircle }, active: { label: 'Picked', color: '#8b5cf6', icon: MdLocalShipping }, onhold: { label: 'On Hold', color: '#8b5cf6', icon: MdHistoryToggleOff }, closed: { label: 'Closed', color: '#06b6d4', icon: MdCheckCircle }, delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, failed: { label: 'Failed', color: '#991b1b', icon: MdCancel }, cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } }; // Top-level pill tabs. const ORDERS_STATUS_TABS = [ { idx: 0, status: 'created', label: 'Created', color: BRAND, icon: MdLocalShipping, countKey: 'created' } ]; const StatusBadge = ({ status }) => { const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || { label: status || '—', color: DT.textMuted, icon: MdHistoryToggleOff }; const Icon = meta.icon; return ( {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 navigate = useNavigate(); const location = useLocation(); const tid = localStorage.getItem('tenantid'); const loadMoreRef = useRef(); const containerRef = useRef(); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); const [tabvalue, setTabvalue] = useState(0); const [tabstatus, setTabstatus] = useState('Created'); const [currentStatus, setCurrentStatus] = useState('created'); const [cancelOpen, setCancelOpen] = useState(false); const [orderheaderid, setOrderheaderid] = useState(''); const locationId = 0; const locoName = 'All Locations'; const [dateOpen, setDateOpen] = useState(false); const [datestatus, setDatestatus] = useState('Today'); const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); const [searchword, setSearchword] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); // Floating button and Dialog states const [speedDialOpen, setSpeedDialOpen] = useState(false); const [multiDeleteDialog, setMultiDeleteDialog] = useState(false); const [createloader, setCreateloader] = useState(false); const [isLoading, setIsLoading] = useState(false); const [deliverylist, setDeliverylist] = useState([]); useEffect(() => { const handler = setTimeout(() => { setDebouncedSearch(searchword); }, 400); return () => clearTimeout(handler); }, [searchword]); const handleChangetab = (e, i) => { setSearchword(''); setRowsPerPage(10); setTabvalue(i); const tab = ORDERS_STATUS_TABS[i]; setTabstatus(tab.label); setCurrentStatus(tab.status); setPage(0); }; const textFieldRef = useRef(null); useHotkeyFocus(textFieldRef, 'k'); // React Queries const { data: percentageData, isLoading: fetchpercentageIsLoading, refetch: percentagedataRefetch } = useQuery({ queryKey: ['percentageData', locationId, startdate, enddate, tid, locationId], queryFn: fetchPercentageData, enabled: true, refetchInterval: 15000 }); const { data: ordersCountData, refetch: orderscountRefetch } = useQuery({ queryKey: ['ordersCount', locationId, startdate, enddate, currentStatus, tid, locationId], queryFn: fetchorderscount, refetchOnMount: true, refetchOnWindowFocus: true, refetchInterval: 15000 }); const { data: autoRiders } = useQuery({ queryKey: ['getallriders'], queryFn: getallriders, refetchOnMount: true, refetchOnWindowFocus: true }); const cancelorder = async () => { await axios .put(`${process.env.REACT_APP_URL}/orders/updateorder`, { orderheaderid: orderheaderid, orderstatus: 'cancelled', cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss') }) .then((res) => { if (res.data.status) { enqueueSnackbar('Order Cancelled Successfully', { variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); refetchOrders(); orderscountRefetch(); percentagedataRefetch(); setCancelOpen(false); } }) .catch((err) => { console.log(err); }); }; const fetchOrders = async ({ pageParam = 1 }) => { const res = await axios.get( `${process.env.REACT_APP_URL}/orders/tenant/getorders/?tenantid=${tid}&locationid=${locationId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${debouncedSearch}` ); return { data: res.data.details, nextPage: res.data.details.length === rowsPerPage ? pageParam + 1 : undefined }; }; const { data: rowdata, fetchNextPage, isLoading: isLoadingGetOrders, hasNextPage, isFetchingNextPage, refetch: refetchOrders } = useInfiniteQuery({ queryKey: [tabstatus, startdate, enddate, page, rowsPerPage, debouncedSearch, locationId], queryFn: fetchOrders, getNextPageParam: (lastPage) => lastPage.nextPage, refetchInterval: 15000 }); const rows = rowdata ? rowdata.pages.flatMap((p) => p.data) : []; useEffect(() => { if (!hasNextPage) return; const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { fetchNextPage(); } }, { root: null, rootMargin: '0px 0px 400px 0px', threshold: 0 } ); if (loadMoreRef.current) observer.observe(loadMoreRef.current); return () => { if (loadMoreRef.current) observer.unobserve(loadMoreRef.current); }; }, [hasNextPage, fetchNextPage]); const handleScroll = (event) => { const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; if (scrollTop + clientHeight >= scrollHeight - 50) { if (hasNextPage && !isFetchingNextPage) { fetchNextPage(); } } }; // ==============================|| Mutations ||============================== // const createDeliveryMutation = useMutation({ mutationFn: createAutomationDeliveries, onSuccess: (data, variables) => { enqueueSnackbar('Orders Optimised Successfully', { variant: 'success', autoHideDuration: 2000, anchorOrigin: { vertical: 'top', horizontal: 'right' } }); orderscountRefetch(); refetchOrders(); setCreateloader(false); navigate('/nearle/dispatch/preview', { state: { dispatchPreviewData: data, aiMode: 1, selectedMode: { value: 1 }, deliveryData: variables?.deliveries || [], appId: locationId, startdate: startdate, tenantId: tid, autoRiders: autoRiders || [] } }); }, onError: (error) => { enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 4000 }); setCreateloader(false); }, onSettled: () => { setCreateloader(false); setIsLoading(false); } }); const cancelMultipleOrderMutation = useMutation({ mutationFn: cancelMultipleOrder, onSuccess: (data) => { if (data.status) { setMultiDeleteDialog(false); enqueueSnackbar('Orders Cancelled Successfully', { variant: 'success', autoHideDuration: 2000 }); refetchOrders(); orderscountRefetch(); percentagedataRefetch(); setDeliverylist([]); } }, onError: (error) => { enqueueSnackbar(error.message, { variant: 'error', autoHideDuration: 4000 }); } }); const handleCreateDelivery = async () => { if (rows.length === 0) return; setIsLoading(true); setCreateloader(true); const deliveryData = rows.map((val) => ({ ...val, deliveryid: 0, deliverydate: dayjs(val.deliverydate).utc().format('YYYY-MM-DD HH:mm:ss'), assigntime: dayjs().format('YYYY-MM-DD HH:mm:ss'), orderstatus: 'pending', orderamount: val.deliverycharge, droplat: val.deliverylat, droplon: val.deliverylong, pickuplat: val.pickuplat, pickuplon: val.pickuplong, ordernotes: val.ordernotes, deliverycharges: val.deliverycharge, pickuplocation: val.pickupsuburb, deliverylocation: val.deliverysuburb })); createDeliveryMutation.mutate({ deliveries: deliveryData, selectedMode: { value: 1 }, hypertuning_params: 'balanced', absent_riders: [] }); }; // KPI tile definitions. const kpiCards = [ { key: 'created', label: 'Created Orders', color: BRAND, icon: MdLocalShipping, value: percentageData?.created, percentage: percentageData?.percentage1 }, { key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdHourglassEmpty, value: percentageData?.uncoveredOrders, percentage: percentageData?.percentage2 } ]; return ( {(fetchpercentageIsLoading || isLoadingGetOrders || isLoading || createloader) && ( <> )} {rows.length > 0 && currentStatus === 'created' && ( )} {/* ============================================= || Header (compact) || ============================================= */} Orders Live · {locoName} · {datestatus} {/* ============================================= || Status Tabs + Search (compact) || ============================================= */} {ORDERS_STATUS_TABS.map((t) => { const Icon = t.icon; const active = tabvalue === t.idx; const count = ordersCountData?.[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} ); })} {/* ============================================= || Table (dense, sticky header) || ============================================= */} # Order Location Pickup Drop Qty COD Kms Charges Notes Status {currentStatus === 'created' && Actions} {(isLoadingGetOrders || createloader) && rows.length === 0 && Array.from({ length: 10 }).map((_, idx) => ( {Array.from({ length: currentStatus === 'created' ? 12 : 11 }).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) => { const isItemSelected = !!deliverylist.find((res) => res.orderheaderid === row.orderheaderid); const handleCheckbox = (e) => { if (e.target.checked) { setDeliverylist((prev) => [...prev, { ...row, sno: prev.length + 1 }]); } else { setDeliverylist((prev) => prev.filter((item) => item.orderheaderid !== row.orderheaderid).map((item, i) => ({ ...item, sno: i + 1 })) ); } }; return ( {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); }} /> {/* ============================================= || Cancel Multiple Orders Dialog || ============================================= */} setMultiDeleteDialog(false)} maxWidth="xs" PaperProps={{ sx: { borderRadius: 3 } }}> Cancel Selected Orders Are you sure you want to cancel the {deliverylist.length} selected orders? This action cannot be undone.
); }; export default Orders;