diff --git a/src/assets/images/aiImage.png b/src/assets/images/aiImage.png new file mode 100644 index 0000000..ba01f74 Binary files /dev/null and b/src/assets/images/aiImage.png differ diff --git a/src/components/nearle_components/LoaderWithImage.js b/src/components/nearle_components/LoaderWithImage.js new file mode 100644 index 0000000..10e1395 --- /dev/null +++ b/src/components/nearle_components/LoaderWithImage.js @@ -0,0 +1,34 @@ +// LoaderWithImage.jsx +import React from 'react'; +import { Box, CircularProgress } from '@mui/material'; +import nelogo from '../../assets/images/logo-sm.png'; + +export default function LoaderWithImage({ size = 70, imgSize = 40, alt = 'loader' }) { + return ( + + + + + {alt} + + + ); +} diff --git a/src/components/nearle_components/MobileCard.js b/src/components/nearle_components/MobileCard.js new file mode 100644 index 0000000..31e2f69 --- /dev/null +++ b/src/components/nearle_components/MobileCard.js @@ -0,0 +1,140 @@ +import PropTypes from 'prop-types'; +import { Box, Paper, Stack, Typography } from '@mui/material'; + +// ============================================================================ +// MobileCard — shared primitives that turn a desktop data-table row into an +// app-style card on phones. Used by every operator list page (deliveries, +// orders, customers, riders, tenants, …) so the mobile experience is +// consistent. Purely presentational: pages keep their own data + handlers and +// just slot content into these shells. Desktop layouts are untouched — these +// only render inside an `isMobile` branch. +// +// Tokens mirror the `DT` block in deliveries.js so cards match page surfaces. +// ============================================================================ +const BORDER = '#e2e8f0'; +const MUTED = '#94a3b8'; +const PRIMARY_TEXT = '#0f172a'; + +// Vertical list wrapper — drop-in replacement for / +// on mobile. `scroll` makes it an internal scroll region (matches the table's +// maxHeight behaviour); omit it to let the page scroll naturally. +export const MobileCardList = ({ children, scroll = false, onScroll, sx, ...rest }) => ( + + {children} + +); + +MobileCardList.propTypes = { + children: PropTypes.node, + scroll: PropTypes.bool, + onScroll: PropTypes.func, + sx: PropTypes.object +}; + +// Card shell — coloured accent rail on the left, a header slot (status badge / +// title / action buttons), then any field grid / collapse content as children. +export const MobileCard = ({ accent = '#662582', header, footer, selected = false, onClick, children, sx }) => ( + + + + {header} + {children} + {footer} + + +); + +MobileCard.propTypes = { + accent: PropTypes.string, + header: PropTypes.node, + footer: PropTypes.node, + selected: PropTypes.bool, + onClick: PropTypes.func, + children: PropTypes.node, + sx: PropTypes.object +}; + +// Grid wrapper for MobileField cells. Two columns by default; pass `columns` +// to change. Keeps every card's body alignment identical. +export const MobileFieldGrid = ({ children, columns = 2, sx }) => ( + + {children} + +); + +MobileFieldGrid.propTypes = { + children: PropTypes.node, + columns: PropTypes.number, + sx: PropTypes.object +}; + +// A single label/value cell. `full` makes it span the whole row; `value` can be +// a string/number or any node (chip, stack, etc.). +export const MobileField = ({ label, value, children, full = false, align = 'left' }) => ( + + + {label} + + + {children !== undefined ? ( + children + ) : ( + + {value ?? '—'} + + )} + + +); + +MobileField.propTypes = { + label: PropTypes.node, + value: PropTypes.node, + children: PropTypes.node, + full: PropTypes.bool, + align: PropTypes.string +}; diff --git a/src/menu-items/nearle.js b/src/menu-items/nearle.js index a92de17..be8a356 100644 --- a/src/menu-items/nearle.js +++ b/src/menu-items/nearle.js @@ -6,6 +6,7 @@ import { TbListDetails } from 'react-icons/tb'; import { LiaFileInvoiceSolid } from 'react-icons/lia'; import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined'; import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined'; +import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined'; // assets import { @@ -64,6 +65,13 @@ const nearle = { url: '/nearle/orders', icon: AiOutlineDashboard }, + { + id: 'deliveries', + title: , + type: 'item', + url: '/nearle/deliveries', + icon: MopedOutlinedIcon + }, { id: 'locations', title: , diff --git a/src/menu-items/other.js b/src/menu-items/other.js index bebd5e9..b783af4 100644 --- a/src/menu-items/other.js +++ b/src/menu-items/other.js @@ -4,6 +4,7 @@ import { AiOutlineBarChart } from 'react-icons/ai'; import { AiOutlineDashboard } from 'react-icons/ai'; import { TbListDetails } from 'react-icons/tb'; import { LiaFileInvoiceSolid } from 'react-icons/lia'; +import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined'; // assets import { @@ -60,6 +61,13 @@ const other = { url: 'nearle/orders', icon: AiOutlineDashboard }, + { + id: 'deliveries', + title: , + type: 'item', + url: 'nearle/deliveries', + icon: MopedOutlinedIcon + }, { id: 'customers', title: , diff --git a/src/pages/nearle/api/api.js b/src/pages/nearle/api/api.js index e3c31a2..399b6b8 100644 --- a/src/pages/nearle/api/api.js +++ b/src/pages/nearle/api/api.js @@ -24,6 +24,34 @@ export const fetchOrders = async ({ pageParam = 1, queryKey }) => { }; }; +// ==============================|| fetchPercentageData (orders) ||============================== // +export const fetchPercentageData = async ({ queryKey }) => { + const [, appId, startdate, enddate, tenantid, locationid] = queryKey; + const response = await axios.get( + `${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}` + ); + const details = response.data.details; + + return { + created: details.created.toString(), + uncoveredOrders: details.pending.toString(), + coveredOrders: details.delivered.toString(), + cancelled: details.cancelled.toString(), + percentage1: (Math.round((details.created / details.total) * 100) || 0).toString(), + percentage2: (Math.round((details.pending / details.total) * 100) || 0).toString(), + percentage3: (Math.round((details.delivered / details.total) * 100) || 0).toString(), + percentage4: (Math.round((details.cancelled / details.total) * 100) || 0).toString() + }; +}; + +// ==============================|| fetchorderscount (orders) ||============================== // +export const fetchorderscount = async ({ queryKey }) => { + const [, appId, startdate, enddate, currentStatus, tenantid, locationid] = queryKey; + const url = `${process.env.REACT_APP_URL}/orders/getordersummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}&status=${currentStatus}`; + const response = await axios.get(url); + return response.data.details; +}; + // ==============================|| fetchOrderSummary (orders)||============================== // export const fetchOrderSummary = async () => { const response = await axios.get(`${process.env.REACT_APP_URL}/orders/getordersummary`); @@ -385,12 +413,12 @@ export const createAutomationDeliveries = async (variables) => { const absentRiders = Array.isArray(variables.absent_riders) ? variables.absent_riders : []; const url = - variables.selectedMode.value == 1 + variables.selectedMode?.value == 1 ? `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params=${variables.hypertuning_params}` : `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip`; const body = - variables.selectedMode.value == 1 + variables.selectedMode?.value == 1 ? { deliveries: variables.deliveries, absent_riders: absentRiders } : { ...(variables.data || {}), absent_riders: absentRiders }; @@ -431,3 +459,37 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => { nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined }; }; + +// Fetch payment types. +export const fetchPaymentType = async () => { + const { data } = await axios.get(`${process.env.REACT_APP_URL}/utils/getapptypes/?tag=paymentmode`); + return data.details.map((val) => ({ + ...val, + label: val.typename + })); +}; + +// Fetch all riders. +export const getallriders = async () => { + try { + const res = await axios.get(`${process.env.REACT_APP_URL}/partners/getallriders?partnerid=64`); + return res.data.details; + } catch (err) { + console.log('getallriders', err.message); + } +}; + +// Cancel multiple orders. +export const cancelMultipleOrder = async (orderlist) => { + const data = orderlist?.map((e) => ({ + orderheaderid: e.orderheaderid, + orderstatus: 'cancelled', + cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss') + })); + + const response = await axios.put(`${process.env.REACT_APP_URL}/orders/updatemultipleorders`, data); + return response.data; +}; + + + diff --git a/src/pages/nearle/deliveries/deliveries.js b/src/pages/nearle/deliveries/deliveries.js new file mode 100644 index 0000000..b108ee2 --- /dev/null +++ b/src/pages/nearle/deliveries/deliveries.js @@ -0,0 +1,1104 @@ +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, + TableContainer +} 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 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 { useQuery, useInfiniteQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router-dom'; + +// ============================================================================ +// 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'; + +// Semantic per-row status palette — colors per brand standard: +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 DELIVERIES_STATUS_TABS = [ + { idx: 0, status: 'pending', label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, countKey: 'pending' }, + { idx: 1, status: 'delivered', label: 'Delivered', color: '#10b981', icon: MdCheckCircle, countKey: 'delivered' }, + { idx: 2, status: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdCancel, countKey: 'cancelled' } +]; + +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 Deliveries = () => { + const navigate = useNavigate(); + 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('Pending'); + const [currentStatus, setCurrentStatus] = useState('pending'); + 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(''); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedSearch(searchword); + }, 400); + return () => clearTimeout(handler); + }, [searchword]); + + const handleChangetab = (e, i) => { + setSearchword(''); + setRowsPerPage(10); + setTabvalue(i); + const tab = DELIVERIES_STATUS_TABS[i]; + setTabstatus(tab.label); + setCurrentStatus(tab.status); + setPage(0); + }; + + const textFieldRef = useRef(null); + useHotkeyFocus(textFieldRef, 'k'); + + const fetchDeliverySummary = async () => { + const response = await axios.get( + `${process.env.REACT_APP_URL}/deliveries/deliverysummary?tenantid=${tid}&locationid=${locationId}&fromdate=${startdate}&todate=${enddate}` + ); + const details = response.data.details; + return { + created: (details.created ?? 0).toString(), + uncoveredOrders: (details.pending ?? 0).toString(), + coveredOrders: (details.delivered ?? 0).toString(), + cancelled: (details.cancelled ?? 0).toString(), + percentage1: (Math.round((details.created / details.total) * 100) || 0).toString(), + percentage2: (Math.round((details.pending / details.total) * 100) || 0).toString(), + percentage3: (Math.round((details.delivered / details.total) * 100) || 0).toString(), + percentage4: (Math.round((details.cancelled / details.total) * 100) || 0).toString() + }; + }; + + // React Queries + const { + data: percentageData, + isLoading: fetchpercentageIsLoading, + refetch: percentagedataRefetch + } = useQuery({ + queryKey: ['deliveryPercentageData', locationId, startdate, enddate, tid], + queryFn: fetchDeliverySummary, + enabled: true, + refetchInterval: 15000 + }); + + 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('Delivery Cancelled Successfully', { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + refetchDeliveries(); + percentagedataRefetch(); + setCancelOpen(false); + } + }) + .catch((err) => { + console.log(err); + }); + }; + + const fetchDeliveries = async ({ pageParam = 1 }) => { + const res = await axios.get( + `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?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: isLoadingGetDeliveries, + hasNextPage, + isFetchingNextPage, + refetch: refetchDeliveries + } = useInfiniteQuery({ + queryKey: ['fetchdeliveries', tabstatus, startdate, enddate, page, rowsPerPage, debouncedSearch, locationId], + queryFn: fetchDeliveries, + 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(); + } + } + }; + + // KPI tile definitions. + const kpiCards = [ + { key: 'pending', label: 'Pending Deliveries', color: '#f59e0b', icon: MdHourglassEmpty, value: percentageData?.uncoveredOrders, percentage: percentageData?.percentage2 }, + { key: 'delivered', label: 'Delivered Deliveries', color: '#10b981', icon: MdCheckCircle, value: percentageData?.coveredOrders, percentage: percentageData?.percentage3 }, + { key: 'cancelled', label: 'Cancelled Deliveries', color: '#ef4444', icon: MdCancel, value: percentageData?.cancelled, percentage: percentageData?.percentage4 } + ]; + + return ( + + {(fetchpercentageIsLoading || isLoadingGetDeliveries) && ( + <> + + + + )} + + {/* ============================================= || Header (compact) || ============================================= */} + + + + + + + + + Deliveries + + + + + Live · {locoName} · {datestatus} + + + + + + + + {/* ============================================= || KPI Cards (compact) || ============================================= */} + + {kpiCards.map((item) => { + const Icon = item.icon; + return ( + + + + + + + {item.label} + + + + {item.value == null ? : item.value} + + {item.percentage != null && item.value != null && ( + + {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} + + + + + + {/* ============================================= || Status Tabs + Search (compact) || ============================================= */} + + + + {DELIVERIES_STATUS_TABS.map((t) => { + const Icon = t.icon; + const active = tabvalue === t.idx; + const count = t.countKey === 'created' ? (percentageData?.created ?? 0) + : t.countKey === 'pending' ? (percentageData?.uncoveredOrders ?? 0) + : t.countKey === 'delivered' ? (percentageData?.coveredOrders ?? 0) + : (percentageData?.cancelled ?? 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} + + + ); + })} + + + + + + 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(''); + refetchDeliveries(); + }} + sx={{ p: 0.25, color: BRAND }} + > + + + )} + + + + + + {/* ============================================= || Table (dense, sticky header) || ============================================= */} + + + + + + # + Delivery Location + Pickup + Drop + Qty + COD + Kms + Charges + Notes + Status + {currentStatus === 'created' && Actions} + + + + + {isLoadingGetDeliveries && + rows.length === 0 && + Array.from({ length: 10 }).map((_, idx) => ( + + {Array.from({ length: currentStatus === 'created' ? 11 : 10 }).map((__, ci) => ( + + + + ))} + + ))} + + {!isLoadingGetDeliveries && rows.length === 0 && ( + + + + + + + + No {currentStatus} deliveries + + + {searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'} + + {searchword && ( + + )} + + + + )} + + {rows.map((row, index) => { + 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 deliveries… + + + ) : ( + + {rows.length} delivery{rows.length === 1 ? '' : 's'} · End of list + + )} + + + + )} + +
+
+
+ + {/* ============================================= || Cancel Order Dialog || ============================================= */} + setCancelOpen(false)} maxWidth="xs" PaperProps={{ sx: { borderRadius: 3 } }}> + + + + + + + Cancel Delivery + + + + + + + Are you sure you want to cancel this delivery? This action cannot be undone. + + + + + + + + + + {/* ============================================= || Date Filter || ============================================= */} + setDateOpen(false)} + onApply={({ startDate, endDate, label }) => { + setStartdate(startDate); + setEnddate(endDate); + setDatestatus(label); + }} + /> +
+ ); +}; + +export default Deliveries; diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css index cbd69c1..496b00e 100644 --- a/src/pages/nearle/dispatch/Dispatch.css +++ b/src/pages/nearle/dispatch/Dispatch.css @@ -2287,6 +2287,10 @@ color: var(--ad-accent, var(--accent)); } +.dispatch-container .adcard-m-time { + color: #10b981; +} + .dispatch-container .adcard-ic { flex-shrink: 0; display: inline-flex; diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js index 25704f7..d7cd66c 100644 --- a/src/pages/nearle/dispatch/Dispatch.js +++ b/src/pages/nearle/dispatch/Dispatch.js @@ -50,6 +50,7 @@ import { MdInsights, MdRefresh } from 'react-icons/md'; +import { CircularProgress } from '@mui/material'; import { fetchDeliveries, fetchAppLocations, getRiderPeriodicLogs, fetchRidersLogs, fetchBatchEfficiency } from '../api/api'; import { STATUS_STYLES, @@ -2609,15 +2610,20 @@ const Dispatch = ({ )} - - - {parseFloat(o.actualkms || o.kms || 0).toFixed(1)} km - {estMeters !== null && ( - - - {formatMeters(estMeters)} - + <> + + + {formatMeters(estMeters)} + + + + {(() => { + const etaMin = estMeters / 1000 / 20 * 60; + return etaMin < 1 ? '< 1 min' : `${Math.ceil(etaMin)} min`; + })()} + + )} @@ -3465,7 +3471,9 @@ const Dispatch = ({ onClick={() => { logger.info('View mode changed: By Zone'); setViewMode('zones'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }} > By Zone - + {!embedded && ( + + )} - - - - setTabValue(v)} sx={{ minHeight: 40 }}> - - - - - - {tabValue === 0 && dispatchPreviewData && ( + {dispatchPreviewData && ( { onChangeRider={(order, focusedRider) => openChangeRider(focusedRider, order)} /> )} - {tabValue === 1 && ( - - {reconcileRiders.length === 0 ? ( - - No rider data available to reconcile. - - ) : ( - - - {hasReconciled - ? 'Steps have been reconciled. The Dispatch tab and Assign payload are updated.' - : 'Click a numbered step to change its rider. Hit Reconcile to verify the corrected steps with the server.'} - - - {reconcileRiders.map((r) => { - const totalKms = r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0); - return ( - - - - - - - - - {r.rider_name} - - - ID: {r.rider_id} - - - - - - - - - - - {r.orders.map((o, idx) => { - const stepNum = o.step ?? idx + 1; - const color = stepColor(Number(stepNum) - 1); - return ( - -
Order #{o.orderid}
-
{o.deliveryaddress || o.deliverysuburb || ''}
-
Click to change rider
-
- } - > - openChangeRider(r, o)} - sx={{ - width: 36, - height: 36, - borderRadius: '50%', - bgcolor: color, - color: '#fff', - display: 'inline-flex', - alignItems: 'center', - justifyContent: 'center', - fontWeight: 800, - fontSize: 14, - cursor: 'pointer', - boxShadow: - '0 0 0 2px rgba(255,255,255,0.6), 0 1px 3px rgba(15,23,42,0.15)', - transition: 'transform 0.15s', - '&:hover': { transform: 'scale(1.08)' } - }} - > - {stepNum} - - - ); - })} - - - ); - })} - - - - - - )} -
- )} - + - diff --git a/src/pages/nearle/orders/OrdersPreview.js b/src/pages/nearle/orders/OrdersPreview.js new file mode 100644 index 0000000..f006bf9 --- /dev/null +++ b/src/pages/nearle/orders/OrdersPreview.js @@ -0,0 +1,768 @@ +import { + Autocomplete, + Button, + Chip, + Divider, + Grid, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Tooltip, + Typography, + Backdrop, + IconButton +} from '@mui/material'; +import React, { Fragment, useEffect, useMemo, useState } from 'react'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; +import { useLocation, useNavigate } from 'react-router-dom'; +import dayjs from 'dayjs'; +import MainCard from 'components/MainCard'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { fetchPaymentType, fetchRidersList, finalCreatedeliveries, notifyRider } from '../api/api'; +import { OpenToast } from 'components/nearle_components/OpenToast'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import Loader from 'components/Loader'; +import CircularLoader from 'components/nearle_components/CircularLoader'; +import { Empty } from 'antd'; +import HoverSocialCard from 'components/cards/statistics/HoverSocialCard'; +import { DashboardFilled } from '@ant-design/icons'; +import { MdDirectionsBike } from 'react-icons/md'; +import { FaMapLocationDot } from 'react-icons/fa6'; +import { HiOutlineArrowLeft } from 'react-icons/hi'; + +var utc = require('dayjs/plugin/utc'); +dayjs.extend(utc); + +// Mobile-only rendering of the optimised-orders preview. Mirrors the exact same +// fields, chips and tooltips as the desktop table rows — no behaviour added, +// purely a card layout for phones. Renders for both aiMode 1 and normal mode; +// the Zone / Rider fields are gated on aiMode just like the table columns. +const MobileOrdersList = ({ list, aiMode }) => { + if (!list || list.length === 0) { + return ( + + + + ); + } + return ( + + {list.map((val, index) => { + const typeColor = + val.ordertype == 'Economy' ? 'success' : val.ordertype == 'Risky' ? 'error' : 'primary'; + return ( + + + #{index + 1} + {aiMode == 1 && } + + + + } + > + + + + + + {val.tenantname} + + + {val.tenantsuburb} + + + {val.applocation} + + + + + + + + + {`${val.locationname}-(${val.locationsuburb})`} + + + + + {val.orderid} + + + + + + + {dayjs(val.orderdate).utc().format('DD/MM/YYYY')} + + + {dayjs(val.orderdate).utc().format('hh:mm A')} + + + + - + + + + {dayjs(val.deliverydate).utc().format('DD/MM/YYYY')} + + + {dayjs(val.deliverydate).utc().format('hh:mm A')} + + + + + + + + + {val.pickupcustomer} + {val.pickupcontactno} + + {val.pickupsuburb || val.pickupaddress.slice(0, 20)} + + + + + + + {val.deliverycustomer} + {val.deliverycontactno} + + {val.deliverysuburb || val.deliveryaddress.slice(0, 20)} + + + + + {val.ordernotes ? : null} + + {aiMode == 1 && ( + + {val.username} + ID : {val.userid} + + )} + + + + + + + + + + + + + + + + + + + + + + + + + ); + })} + + ); +}; + +const OrdersPreview = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const navigate = useNavigate(); + const location = useLocation(); + console.log('location.state', location.state); + const [rider, setRider] = useState(null); + const [payment, setPayment] = useState(null); + const [finaldeliveryList, setFinalDeliveryList] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const deliverylist = location.state?.deliverylist; + const zoneData = location.state?.zoneData; + const metaData = location.state?.metaData; + const riderToken = location.state?.riderToken; + const appId = location.state?.appId; + const aiMode = location.state?.aiMode; + const reassignOrders = location.state?.reassignOrders; + + useEffect(() => { + console.log('aiMode', aiMode); + console.log('riderToken', riderToken); + console.log('zoneData', zoneData); + console.log('metaData', metaData); + console.log('reassignOrders', reassignOrders); + }, []); + + useEffect(() => { + if (!deliverylist?.length) return; + const updateDeliveryAmtList = deliverylist.map((list) => { + const cumulativeKms = Number(list.cumulativekms || 0); + const minKm = Number(list.minkm || 0); + const basePrice = Number(list.baseprice || 0); + const pricePerKm = Number(list.priceperkm || 0); + if (cumulativeKms <= minKm) { + return { + ...list, + deliveryamt: basePrice + }; + } + return { + ...list, + deliveryamt: (cumulativeKms - minKm) * pricePerKm + basePrice + }; + }); + + setFinalDeliveryList(updateDeliveryAmtList); + console.log('finaldeliveryList', updateDeliveryAmtList); + }, [deliverylist]); + + // ==============================|| fetchPaymentType ||============================== // + + const { + data: paymentModes = [], + isLoading: paymentModesLoading + } = useQuery({ + queryKey: ['paymentmodes'], + queryFn: fetchPaymentType + }); + + // ==============================|| fetchRidersList ||============================== // + + const { + data: ridersList = [], + isLoading: ridersListLoading + } = useQuery({ + queryKey: ['ridersList', appId], // Unique key for caching & re-fetching + queryFn: fetchRidersList, + enabled: appId !== 0 // Ensures query runs only when appId is valid + }); + + const getRiderName = async (userid) => { + await ridersList.map((rider) => { + if (rider.userid == userid) { + return rider.firstname; + } + }); + }; + + // ======================================================= || notifyRiderMutation || ======================================================= + + const notifyRiderMutation = useMutation({ + mutationFn: notifyRider, // Using the separate function + onSuccess: () => { + OpenToast('Notification sent Successfully', 'success', 2000); + }, + onError: (error) => { + OpenToast(error.message, 'error', 2000); + } + }); + + const createNormalDeliveryMutation = useMutation({ + mutationFn: finalCreatedeliveries, // for optimised delivery create + + onSuccess: (data, variables) => { + console.log('data', data); + console.log('varialbles', variables); + notifyRiderMutation.mutate(rider?.userfcmtoken || riderToken); // Call notifyRider after success + if (data.status == 'accepted') { + OpenToast('Delivery Created Successfully', 'success', 2000); + } + setTimeout(() => { + setIsLoading(false); + navigate('/nearle/orders'); + }, 2000); + }, + onError: (error) => { + OpenToast(error.message, 'error', 4000); + } + }); + + const handleManualCreateDelivery = async () => { + setIsLoading(true); + createNormalDeliveryMutation.mutate({ + deliveries: finaldeliveryList + }); + }; + + return ( + + + navigate('/nearle/orders')} + sx={{ + backgroundColor: 'action.hover', + color: 'text.primary', + '&:hover': { + backgroundColor: 'action.selected' + } + }} + > + + {' '} + + + Assign Orders + + + } + secondary={ + + } + > + {(paymentModesLoading || ridersListLoading || isLoading) && ( + <> + + + + )} + { + theme.zIndex.drawer + 1 + }} + open={paymentModesLoading || ridersListLoading || isLoading} // when loader = true, backdrop covers the page + > + + + } + + {aiMode == 1 && ( + + + + } + color={theme.palette.success.main} + sx={{ cursor: 'pointer' }} + /> + + + } + color={theme.palette.warning.main} + /> + + + } + color={theme.palette.info.main} + /> + + + } + color={theme.palette.error.main} + /> + + + + )} + {isMobile ? ( + + ) : ( + + + + + # + {aiMode == 1 && ( + Zone + )} + Tenant + order Location + Pickup + Delivery + Notes + {aiMode == 1 && ( + Rider + )} + + Type + + + Profit + + + Charges + + + KMS + + + + + {finaldeliveryList?.length == 0 && ( + + + + + + )} + {finaldeliveryList && aiMode == 1 // ai mode , ai automation + ? finaldeliveryList?.map((val, index) => { + return ( + + + + {index + 1} + + {aiMode == 1 && ( + + + + )} + + + + {val.tenantname} + + + {val.tenantsuburb} +
+
+ + + {val.applocation} + +
+
+ + + + {`${val.locationname}-(${val.locationsuburb})`} + + + + + {val.orderid} + + + + + + {dayjs(val.orderdate).utc().format('DD/MM/YYYY')} + + + {dayjs(val.orderdate).utc().format('hh:mm A')} + + + - + + + {dayjs(val.deliverydate).utc().format('DD/MM/YYYY')} + + + + {dayjs(val.deliverydate).utc().format('hh:mm A')} + + + + + + + + {val.pickupcustomer} + {val.pickupcontactno} + + {val.pickupsuburb || val.pickupaddress.slice(0, 20)} + + + + + + + + {val.deliverycustomer} + {val.deliverycontactno} + + {val.deliverysuburb || val.deliveryaddress.slice(0, 20)} + + + + + {val.ordernotes} + {aiMode == 1 && ( + + {val.username} + ID : {val.userid} + + )} + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ); + }) + : // normal optimisation + finaldeliveryList?.map((val, index) => { + return ( + + + + {index + 1} + + + + + {val.tenantname} + + + {val.tenantsuburb} +
+
+ + + {val.applocation} + +
+
+ + + + {`${val.locationname}-(${val.locationsuburb})`} + + + + + {val.orderid} + + + + + + {dayjs(val.orderdate).utc().format('DD/MM/YYYY')} + + + {dayjs(val.orderdate).utc().format('hh:mm A')} + + + - + + + {dayjs(val.deliverydate).utc().format('DD/MM/YYYY')} + + + + {dayjs(val.deliverydate).utc().format('hh:mm A')} + + + + + + + + {val.pickupcustomer} + {val.pickupcontactno} + + {val.pickupsuburb || val.pickupaddress.slice(0, 20)} + + + + + + + + {val.deliverycustomer} + {val.deliverycontactno} + + {val.deliverysuburb || val.deliveryaddress.slice(0, 20)} + + + + + {val.ordernotes} + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ); + })} +
+
+
+ )} + + {aiMode == 0 && ( + + + } + onChange={(event, newValue, reason) => { + if (reason === 'clear') { + setPayment(null); + return; + } + if (newValue) { + console.log('Selected:', newValue); + setPayment(newValue); + const newList = finaldeliveryList?.map((list) => ({ + ...list, + paymenttype: newValue.apptypeid // merge selected rider into each list item + })); + setFinalDeliveryList(newList); + } + }} + /> + + + } + onChange={(event, newValue, reason) => { + if (reason === 'clear') { + setRider(null); + return; + } + if (newValue) { + setRider(newValue); + console.log('Selected:', newValue); + const newList = finaldeliveryList?.map((list) => ({ + ...list, + userid: newValue.userid, + userfcmtoken: newValue.userfcmtoken + })); + setFinalDeliveryList(newList); + } + }} + /> + + + )} + + + + + +
+ ); +}; + +export default OrdersPreview; diff --git a/src/pages/nearle/orders/OrdersTableSkeleton.js b/src/pages/nearle/orders/OrdersTableSkeleton.js new file mode 100644 index 0000000..80bb899 --- /dev/null +++ b/src/pages/nearle/orders/OrdersTableSkeleton.js @@ -0,0 +1,49 @@ +import { TableRow, TableCell, Skeleton, Stack } from '@mui/material'; + +export const OrdersTableSkeleton = ({ rowsPerPage = 5, col = 1 }) => { + return ( + <> + {Array.from(new Array(rowsPerPage)).map((_, index) => ( + + {/* Checkbox */} + + + + + {/* Serial Number */} + + + + + {/* Delivery Info */} + {Array.from({ length: col }).map((_, index) => ( + + + + + + + ))} + + {/* Notes */} + + + + + {/* Order Status */} + + + + + {/* Actions */} + + + + + + + + ))} + + ); +}; diff --git a/src/pages/nearle/orders/multipleOrders.js b/src/pages/nearle/orders/multipleOrders.js index 31f6b0f..f95156d 100644 --- a/src/pages/nearle/orders/multipleOrders.js +++ b/src/pages/nearle/orders/multipleOrders.js @@ -1,23 +1,12 @@ -import React from 'react'; -import Loader from 'components/Loader'; -import { useEffect, useState, Fragment, useRef } from 'react'; -import { useTheme } from '@mui/material/styles'; -import MainCard from 'components/MainCard'; +import React, { useEffect, useState, useRef } from 'react'; import axios from 'axios'; -import ClearIcon from '@mui/icons-material/Clear'; -import { SearchOutlined, CloseOutlined, ExclamationCircleOutlined, FileAddOutlined } from '@ant-design/icons'; -import { Empty } from 'antd'; -import MyLocationIcon from '@mui/icons-material/MyLocation'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import dayjs from 'dayjs'; -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); -import { enqueueSnackbar } from 'notistack'; -import { useNavigate } from 'react-router'; import Papa from 'papaparse'; import * as XLSX from 'xlsx'; +import dayjs from 'dayjs'; +import { useNavigate } from 'react-router'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import { enqueueSnackbar } from 'notistack'; import { FormControl, @@ -26,10 +15,10 @@ import { Typography, Stack, Box, + Card, Button, TextField, Autocomplete, - Chip, Divider, Dialog, DialogTitle, @@ -48,660 +37,330 @@ import { TableRow, Paper, TableHead, - FormLabel, - RadioGroup, - Radio, Backdrop, - List, - ListItem, - ListItemText + Chip, + Tooltip } from '@mui/material'; +import { DatePicker } from '@mui/x-date-pickers/DatePicker'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import ClearIcon from '@mui/icons-material/Clear'; +import MyLocationIcon from '@mui/icons-material/MyLocation'; +import { + SearchOutlined, + CloseOutlined, + ExclamationCircleOutlined, + FileAddOutlined, + CalendarOutlined, + ClockCircleOutlined, + FileTextOutlined, + InboxOutlined +} from '@ant-design/icons'; +import { Empty } from 'antd'; +import { FaUser, FaTruck, FaUsers, FaPaperPlane, FaRoute, FaMoneyBillWave, FaBoxes, FaReceipt } from 'react-icons/fa'; +import { FaLocationDot } from 'react-icons/fa6'; +import { MdOutlineCloudUpload } from 'react-icons/md'; + +import Loader from 'components/Loader'; import CircularLoader from 'components/nearle_components/CircularLoader'; +import './OrdersRedesign.css'; + +var utc = require('dayjs/plugin/utc'); +dayjs.extend(utc); + +const cellHeaderSx = { fontSize: 11.5, fontWeight: 700, color: '#475569', py: 0.75, px: 1 }; +const cellBodySx = { fontSize: 12, py: 0.6, px: 1 }; const MultipleOrders = () => { const navigate = useNavigate(); const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); const locationRef = useRef(null); - const tenantRef = useRef(null); const userid = localStorage.getItem('userid'); + // tenantid is fixed for this app — comes from the logged-in session + const tenantid = localStorage.getItem('tenantid'); + + // ============================== state ============================== const [locations, setLocations] = useState([]); - const [tenantlist, setTenantlist] = useState([]); + const [tenantLocations, setTenantlocations] = useState([]); const [loading, setLoading] = useState(false); const [btnLoading, setBtnLoading] = useState(false); + const [appId, setAppId] = useState(0); - const [tenantLocations, setTenantlocations] = useState([]); - // const [tenantid, setTenantid] = useState(0); - const tenantid = localStorage.getItem('tenantid'); const [locationid, setLocationid] = useState(0); + const [locationValue, setLocationValue] = useState(null); + const [basePrice, setBasePrice] = useState(0); const [pricePerKm, setPricePerKm] = useState(0); const [minKm, setMinKm] = useState(0); + const [pickCust, setPickCust] = useState(null); const [dropCust, setDropCust] = useState([]); + const [customerlist, setCustomerlist] = useState([]); const [isCustomerOpen, setIsCustomerOpen] = useState(false); const [searchCustList, setSearchCustList] = useState(''); - const [customerlist, setCustomerlist] = useState([]); + const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY')); - const [timeslotarr, setTimeslotarr] = useState([]); - const [starttime, setStatrttime] = useState(); - const [endtime, setEndtime] = useState(); const [selectedtime, setSelectedtime] = useState(''); - const [alertmessage, setAlertmessage] = useState(''); + const [pickupSlotsList, setPickupSlotsList] = useState(null); + const [pickupSlot, setPickupSlot] = useState(null); + const [otherinstructions, setOtherinstructions] = useState(''); const [admintoken, setAdmintoken] = useState(); + const [totaldist, settotaldist] = useState(0); const [totalAmt, settotalAmt] = useState(0); const [totalQty, settotalQty] = useState(0); const [totalCash, settotalCash] = useState(0); - const [users, setUsers] = useState([]); + const [uploadType, setUploadType] = useState(null); - const [tenantValue, setTenantValue] = useState(null); - const [locationValue, setLocationValue] = useState(null); - const [pickupSlotsList, setPickupSlotsList] = useState(null); - const [pickupSlot, setPickupSlot] = useState(null); + const [users, setUsers] = useState([]); + const [fileName, setFileName] = useState(''); - useEffect(() => { - if (timeslotarr[0]) { - let arr = []; - timeslotarr.map((val) => { - if (dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0) { - arr.push(val); - } - }); - } - }, [timeslotarr]); + const toastCacheRef = useRef({}); - // =============================================== || opentoast || =============================================== + // ============================== toast ============================== const opentoast = (message, variant, time) => { enqueueSnackbar(message, { - variant: variant, + variant, anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: time ? time : 1500 + autoHideDuration: time || 1500 }); - console.log(alertmessage); }; - // 🔹 Smart toast wrapper — prevents duplicate toasts for same message within 3 seconds - let toastCache = {}; - const OpenToast = (message, type = 'info', timeout = 10000) => { + const OpenToast = (message, type = 'info', timeout = 3000) => { const key = `${type}-${message}`; - if (toastCache[key]) return; // skip duplicates - opentoast(message, type, timeout); // your existing toast/snackbar - toastCache[key] = true; - setTimeout(() => delete toastCache[key], 3000); // reset after delay + if (toastCacheRef.current[key]) return; + opentoast(message, type, timeout); + toastCacheRef.current[key] = true; + setTimeout(() => delete toastCacheRef.current[key], 3000); }; - // ==============================|| fetchAppLocations ||============================== // + // ============================== reset chains ============================== + useEffect(() => { + setLocationid(0); + setLocationValue(null); + setDropCust([]); + setUsers([]); + setUploadType(null); + setFileName(''); + setPickupSlotsList(null); + setPickupSlot(null); + setSelectedtime(''); + }, [appId]); + // ============================== fetchAppLocations ============================== const fetchAppLocations = async () => { setLoading(true); - try { - const locationRes = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`); - console.log('fetchAppLocations', locationRes.data.details); - setLocations(locationRes.data.details); + const res = await axios.get(`${process.env.REACT_APP_URL}/partners/getlocations/?userid=${userid}`); + setLocations(res.data.details || []); } catch (err) { - console.log('locationRes', err); OpenToast(err.message, 'error', 5000); } finally { setLoading(false); } }; - useEffect(() => { - fetchAppLocations(); - }, []); - // ===================================================== || fetchtenantinfolist || ===================================================== - - const fetchtenantinfolist = async () => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/tenants/gettenants/?applocationid=${appId}&status=active`) - - .then((res) => { - console.log(res); - if (res.data.status) { - let arr = []; - res.data.details.map((val) => { - arr.push({ - ...val, - label: `${val.tenantname}` - }); - }); - setTenantlist(arr); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - useEffect(() => { - appId && fetchtenantinfolist(); - }, [appId]); - // ============================================= || fetchTenantPricing || ============================================= + useEffect(() => { fetchAppLocations(); }, []); + // ============================== fetchTenantPricing ============================== const fetchTenantPricing = async (id) => { try { - const pricingResponse = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`); - console.log('pricingResponse', pricingResponse.data.details); - setBasePrice(pricingResponse.data.details.baseprice); - setPricePerKm(pricingResponse.data.details.priceperkm); - setMinKm(pricingResponse.data.details.minkm); - } catch (error) { - console.log('fetchTenantPricing error', error); + const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantpricing/?tenantid=${id}`); + const d = res.data.details || {}; + setBasePrice(d.baseprice || 0); + setPricePerKm(d.priceperkm || 0); + setMinKm(d.minkm || 0); + } catch (err) { + console.log('fetchTenantPricing', err); } }; - useEffect(() => { - fetchTenantPricing(tenantid); - }, [tenantid]); - // ============================================= || gettenantlocations (branches) || ============================================= + + useEffect(() => { if (tenantid) fetchTenantPricing(tenantid); }, []); + + // ============================== gettenantlocations ============================== const gettenantlocations = async (id) => { try { const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`); - console.log('gettenantlocations', res.data.details); - if (res.data.details.length == 1) { - setTenantlocations(res.data.details); - setPickCust(res.data.details[0]); - setLocationid(res.data.details[0].locationid); - setLocationValue(res.data.details[0].locationid); - setPickupSlotsList(res.data.details[0].slots); + const details = res.data.details || []; + if (details.length === 1) { + setTenantlocations(details); + setPickCust(details[0]); + setLocationid(details[0].locationid); + setLocationValue(details[0]); + setPickupSlotsList(details[0].slots); } else { - setTenantlocations(res.data.details); + setTenantlocations(details); } } catch (err) { console.log('gettenantlocations', err); } }; - useEffect(() => { - gettenantlocations(tenantid); - }, [tenantid]); - // ========================================================= || clientdetails || ========================================================= + + useEffect(() => { if (tenantid) gettenantlocations(tenantid); }, []); + + // ============================== clientdetails ============================== const clientdetails = async () => { try { - let url = - searchCustList == '' - ? `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tenantid}&pageno=1&pagesize=30` - : `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tenantid}&keyword=${searchCustList}`; - await axios - .get(url) - .then((res) => { - if (res.data.status) { - console.log('clientdetails', res.data.details); - - setCustomerlist(res.data.details); - let arr = []; - res.data.details.map((val) => { - arr.push({ - label: `${val.firstname} | ${val.contactno}`, - ...val - }); - }); - } - }) - .catch((err) => { - console.log(err); - opentoast('server error', 'warning'); - }); + const url = searchCustList === '' + ? `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tenantid}&pageno=1&pagesize=30` + : `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tenantid}&keyword=${searchCustList}`; + const res = await axios.get(url); + if (res.data.status) setCustomerlist(res.data.details || []); } catch (err) { - console.log(err); + opentoast('server error', 'warning'); } }; - useEffect(() => { - if (tenantid) { - clientdetails(); - } - }, [searchCustList.length > 3, searchCustList == '', tenantid]); - // ========================================================= || calculateTotal(dist , charge) || ========================================================= - const calculateTotal = () => { - let a1 = 0; - let a2 = 0; - let a3 = 0; - let a4 = 0; - dropCust.map((customer) => { - a1 += customer.distance; - a2 += customer.totalcharge; - a3 += customer.quantity; - a4 += customer.collectionamt; + useEffect(() => { + if (!tenantid) return; + const t = setTimeout(() => { + if (searchCustList === '' || searchCustList.length > 2) clientdetails(); + }, 250); + return () => clearTimeout(t); + }, [searchCustList, tenantid]); + + // ============================== totals ============================== + useEffect(() => { + let a1 = 0, a2 = 0, a3 = 0, a4 = 0; + dropCust.forEach((c) => { + a1 += Number(c.distance) || 0; + a2 += Number(c.totalcharge) || 0; + a3 += Number(c.quantity) || 0; + a4 += Number(c.collectionamt) || 0; }); settotaldist(a1); settotalAmt(a2); settotalQty(a3); settotalCash(a4); - }; - useEffect(() => { - calculateTotal(); }, [dropCust]); - // ========================================================= || handleCheckboxChange || ========================================================= - const handleCheckboxChange = async (event, customer) => { - setLoading(true); - if (event.target.checked) { - // If the checkbox is checked, calculate the distance and add the customer - try { - const obj = await calculateDistance(customer); - const { roundedDistance, totalcharge } = obj; - // Create a new customer object with the distance property - const updatedCustomer = { - ...customer, - distance: roundedDistance, - totalcharge: totalcharge - }; - - // Add the updated customer object to dropCust - setDropCust((prevDropCust) => [...prevDropCust, updatedCustomer]); - - // Log the rounded distance - // console.log(`Rounded Distance: ${roundedDistance} km`); - } catch (error) { - console.error('Failed to calculate distance:', error); - } finally { - setLoading(false); - } - } else { - // If the checkbox is unchecked, remove the customer from dropCust - setDropCust((prevDropCust) => { - return prevDropCust.filter((cust) => cust.customerid !== customer.customerid); - }); - setLoading(false); - } - }; - // ========================================================= || handleCheckboxChange1 || ========================================================= - // const handleCheckboxChange1 = async (customer) => { - // console.log('customer', customer); - // setLoading(true); - // try { - // const obj = await calculateDistance(customer); - // const { roundedDistance, totalcharge } = obj; - // // Create a new customer object with the distance property - // const updatedCustomer = { - // ...customer, - // distance: roundedDistance, - // totalcharge: totalcharge - // }; - - // // Add the updated customer object to dropCust - // setDropCust((prevDropCust) => [...prevDropCust, updatedCustomer]); - - // // Log the rounded distance - // console.log(`Rounded Distance: ${roundedDistance} km`); - // setLoading(false); - // } catch (error) { - // console.error('Failed to calculate distance:', error); - // } - // }; - const handleCheckboxChange1 = async (customer) => { - console.log('customer', customer); - - setLoading(true); - - try { - setDropCust((prevDropCust) => { - const isAlreadySelected = prevDropCust.some((c) => c.firstname === customer.firstname); - - // 🔴 REMOVE if already exists - if (isAlreadySelected) { - return prevDropCust.filter((c) => c.firstname !== customer.firstname); - } - - // 🟢 ADD if not exists (calculate distance) - return prevDropCust; - }); - - // Only calculate distance if customer is not already added - const alreadyExists = dropCust.some((c) => c.firstname === customer.firstname); - - if (!alreadyExists) { - const obj = await calculateDistance(customer); - const { roundedDistance, totalcharge } = obj; - - const updatedCustomer = { - ...customer, - distance: roundedDistance, - totalcharge - }; - - setDropCust((prevDropCust) => [...prevDropCust, updatedCustomer]); - - console.log(`Rounded Distance: ${roundedDistance} km`); - } - } catch (error) { - console.error('Failed to calculate distance:', error); - } finally { - setLoading(false); - } - }; - - // ========================================================= || calculateDistance || ========================================================= - - // 🔹 Main distance calculation function + // ============================== distance (Google) ============================== const calculateDistance = async (customer) => { - const service = new google.maps.DistanceMatrixService(); - - // Helper: safely get distance matrix - const getDistanceMatrix = (origins, destinations) => { - return new Promise((resolve, reject) => { - // 2; + if (typeof window === 'undefined' || !window.google?.maps?.DistanceMatrixService) { + throw new Error('Google Maps not loaded'); + } + const service = new window.google.maps.DistanceMatrixService(); + const getDistanceMatrix = (origins, destinations) => + new Promise((resolve, reject) => { try { - if (!origins || !destinations) { - return reject(new Error('Origin or destination data missing.')); - } + if (!origins || !destinations) return reject(new Error('Origin or destination data missing.')); service.getDistanceMatrix( { - origins: [new google.maps.LatLng(origins.latitude, origins.longitude)], - destinations: [new google.maps.LatLng(destinations.latitude, destinations.longitude)], + origins: [new window.google.maps.LatLng(origins.latitude, origins.longitude)], + destinations: [new window.google.maps.LatLng(destinations.latitude, destinations.longitude)], travelMode: 'DRIVING', - unitSystem: google.maps.UnitSystem.METRIC + unitSystem: window.google.maps.UnitSystem.METRIC }, (response, status) => { - if (status === 'OK') { - resolve(response); - } else { - reject(new Error(`Google API error: ${status}`)); - } + if (status === 'OK') resolve(response); + else reject(new Error(`Google API error: ${status}`)); } ); } catch (err) { reject(new Error(`Unexpected error inside DistanceMatrixService: ${err.message}`)); } }); - }; try { - // --- Input validation --- - if (!customer || typeof customer !== 'object') { - throw new Error('Invalid customer data: expected an object.'); - } - - if (!pickCust || typeof pickCust !== 'object') { - throw new Error('Origin (pickCust) data missing or invalid.'); - } - - // --- Call Google Maps API --- + if (!customer || typeof customer !== 'object') throw new Error('Invalid customer data.'); + if (!pickCust || typeof pickCust !== 'object') throw new Error('Origin (pickCust) data missing or invalid.'); const response = await getDistanceMatrix(pickCust, customer); - - // --- Validate response structure --- - if (!response.rows?.[0]?.elements?.[0] || !response.rows[0].elements[0].distance?.value) { - throw new Error('Malformed Distance Matrix response: missing distance value.'); - } - // --- Compute distance --- - const distanceInMeters = response.rows[0].elements[0].distance.value; - const distanceInKilometers = distanceInMeters / 1000; - const roundedDistance = Math.round(distanceInKilometers); - - // --- Calculate total charge --- - let totalcharge; - if (roundedDistance < minKm) { - totalcharge = basePrice; - } else { - totalcharge = (roundedDistance - minKm) * pricePerKm + basePrice; - } + const distVal = response?.rows?.[0]?.elements?.[0]?.distance?.value; + if (distVal == null) throw new Error('Malformed Distance Matrix response: missing distance value.'); + const km = distVal / 1000; + const roundedDistance = Math.round(km); + const totalcharge = roundedDistance < minKm ? basePrice : (roundedDistance - minKm) * pricePerKm + basePrice; return { roundedDistance, totalcharge }; } catch (error) { - // --- Categorized smart error handling --- - console.log('on calculateDistance', error.message); - if (error.message.includes('Google API')) { - console.log('🚨 Google Maps API Error:', error.message); - OpenToast('Invalid file format, upload valid file', 'error', 5000); - } else if (error.message.includes('Invalid coordinates')) { - console.log('📍 Invalid coordinate format:', error.message, 3000); - OpenToast('Invalid coordinate format. Check location data.', 'warning'), 3000; - } else if (error.message.includes('Malformed Distance Matrix')) { - console.log('⚠️ Unexpected Google response structure:', error.message); - OpenToast('Google Distance Matrix returned invalid data.', 'error', 3000); - } else if (error.message.includes('Origin') || error.message.includes('customer')) { - console.log('❌ Missing or invalid input data:', error.message); - OpenToast('Missing or invalid input data for distance calculation.', 'warning', 3000); - } else { - console.log('💥 Unexpected error calculating distance:', error); - OpenToast('Unexpected error during distance calculation.', 'error', 3000); - } - - throw error; // keeps your current flow intact + if (error.message.includes('Google API')) OpenToast('Invalid coordinates or Google API error.', 'error', 3000); + else if (error.message.includes('Malformed')) OpenToast('Google Distance Matrix returned invalid data.', 'error', 3000); + else OpenToast('Unexpected error during distance calculation.', 'error', 3000); + throw error; } }; - // ==================================================== || fetchTiming || ==================================================== - const fetchTiming = async () => { + // ============================== checkbox handlers ============================== + const handleCheckboxChange = async (event, customer) => { setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/utils/getapplocations/?applocationid=${appId}`) - .then((res) => { - console.log('fetchTiming', res); - const { opentime, closetime } = res.data.details[0]; - if (res.data.status) { - setStatrttime(`${dayjs().format('MM-DD-YYYY')} ${opentime}`); - setEndtime(`${dayjs().format('MM-DD-YYYY')} ${closetime}`); - console.log('starttime', `${dayjs().format('MM-DD-YYYY')} ${opentime}`); - console.log('endtime', `${dayjs().format('MM-DD-YYYY')} ${closetime} `); - let arr = []; - for ( - let i = `${dayjs().format('MM-DD-YYYY')} ${opentime}`, j = 0; - dayjs(`${dayjs().format('MM-DD-YYYY')} ${closetime} `).diff(i, 'm') >= 0; - j++, i = dayjs(i).add(30, 'm') - ) { - arr.push(i); - } - console.log('setTimeslotarr', arr); - setTimeslotarr(arr); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - useEffect(() => { - if (appId) { - fetchTiming(); - } - }, [appId]); - - const fetchAppAdminTokens = async () => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`) - .then((res) => { - const userfcmtokemArray = res.data.details.applocationadmins.map((admin) => admin.userfcmtokem); // fcm => firebase cloud messaging - console.log('fetchAppAdminTokens', res); - console.log('userfcmtokemArray', userfcmtokemArray); - if (res.data.status) { - setAdmintoken(userfcmtokemArray); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - }; - - useEffect(() => { - if (appId) { - fetchAppAdminTokens(); - } - }, [appId]); - - useEffect(() => { - console.log('pickCust', pickCust); - }, [pickCust]); - useEffect(() => { - console.log('dropCust', dropCust); - }, [dropCust]); - // // ==================================================== || fetchtenantinfo || ==================================================== - // const fetchtenantinfo = async () => { - // setLoading(true); - // console.log('tenantid', tenantid); - - // await axios - // .get(`${process.env.REACT_APP_URL}/tenants/gettenantinfo/?tenantid=${tenantid}`) - // .then((res) => { - // console.log('fetchtenantinfo', res); - // if (res.data.status) { - // setTenantid(res.data.details.tenantid); - // } - // setLoading(false); - // }) - // .catch((err) => { - // console.log(err); - // setLoading(false); - // }); - // }; - // useEffect(() => { - // if (tenantid) { - // fetchtenantinfo(); - // } - // }, [tenantid]); - // ================================================== || sendnotifications || ================================================== - const sendnotifications = async () => { - setLoading(true); - await axios - .post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, { - priority: 'high', - registration_ids: admintoken, - data: { - accessid: process.env.REACT_APP_RIDER_ACCESS_ID - }, - notification: { - title: 'Nearle Merchant', - body: 'An Order has been placed successfully,kindly process the same', - sound: 'ring' - } - }) - .then((res) => { - console.log(res); - if (res.data.message == 'Success') { - enqueueSnackbar('Notification sent Successfully', { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 1000 - }); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - enqueueSnackbar(err.message, { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 1000 - }); - setLoading(false); - }); - }; - - const cleanReceiverName = (name) => { - if (typeof name !== 'string') return name; - return name.replace(/^[\d.\s]+/, '').trim(); - }; - - const handleFileUpload = (event) => { - console.log('Normal upload started...'); try { - const file = event.target.files?.[0]; - if (!file) { - opentoast('No file selected.', 'warning'); - return; - } - - const fileName = file.name.toLowerCase(); - const isCSV = fileName.endsWith('.csv'); - const isExcel = fileName.endsWith('.xls') || fileName.endsWith('.xlsx'); - - // Invalid file - if (!isCSV && !isExcel) { - opentoast('Invalid file type. Please upload a CSV or Excel file.', 'warning'); - return; - } - - // ---------------------- CSV ---------------------- - if (isCSV) { - Papa.parse(file, { - header: true, - dynamicTyping: true, - skipEmptyLines: true, - complete: (results) => { - const data = results.data || []; - if (data.length === 0) { - opentoast('CSV file is empty or invalid.', 'warning'); - setUsers([]); - return; - } - const cleanedData = data.map((row) => ({ - ...row, - firstname: cleanReceiverName(row.firstname) - })); - console.log('✅ Parsed CSV Data:', cleanedData); - setUsers(cleanedData); - opentoast('CSV file uploaded successfully, ✅', 'success', 3000); - opentoast(' Press Continue to add delivery customers', 'warning', 5000); - }, - error: (error) => { - console.error('❌ CSV Parse Error:', error.message); - opentoast(`CSV parsing failed: ${error.message}`, 'warning'); - } - }); - } - - // ---------------------- EXCEL (.xls / .xlsx) ---------------------- - if (isExcel) { - const reader = new FileReader(); - - reader.onload = (e) => { - try { - const data = e.target.result; - - // Use correct mode for binary Excel formats - const workbook = XLSX.read(data, { - type: 'binary', - cellDates: true, - cellNF: false, - cellText: false - }); - const firstSheet = workbook.SheetNames[0]; - const worksheet = workbook.Sheets[firstSheet]; - const jsonData = XLSX.utils.sheet_to_json(worksheet, { defval: '' }); - - if (!jsonData || jsonData.length === 0) { - opentoast('Excel file is empty or invalid.', 'warning'); - setUsers([]); - return; - } - - const cleanedData = jsonData.map((row) => ({ - ...row, - firstname: cleanReceiverName(row.firstname) - })); - - console.log('✅ Parsed Excel Data:', cleanedData); - setUsers(cleanedData); - opentoast('Excel file uploaded successfully ✅, press continue', 'success', 3000); - } catch (err) { - console.error('❌ Excel Parse Error:', err); - opentoast(`Error reading Excel: ${err.message}`, 'warning'); - } - }; - - // ✅ Key fix: use readAsBinaryString for both .xls & .xlsx - reader.readAsBinaryString(file); + if (event.target.checked) { + const { roundedDistance, totalcharge } = await calculateDistance(customer); + setDropCust((prev) => [...prev, { ...customer, distance: roundedDistance, totalcharge }]); + } else { + setDropCust((prev) => prev.filter((c) => c.customerid !== customer.customerid)); } } catch (err) { - console.error('Unexpected error during file upload:', err.message); - opentoast(`Unexpected error: ${err.message}`, 'warning'); + console.error('Failed to calculate distance:', err); + } finally { + setLoading(false); } }; - // your header mapping + const handleCheckboxChange1 = async (customer) => { + let wasSelected = false; + setDropCust((prev) => { + wasSelected = prev.some((c) => c.firstname === customer.firstname); + if (wasSelected) return prev.filter((c) => c.firstname !== customer.firstname); + return prev; + }); + if (wasSelected) return; + setLoading(true); + try { + const { roundedDistance, totalcharge } = await calculateDistance(customer); + setDropCust((prev) => { + if (prev.some((c) => c.firstname === customer.firstname)) return prev; + return [...prev, { ...customer, distance: roundedDistance, totalcharge }]; + }); + } catch (err) { + console.error('Failed to calculate distance:', err); + } finally { + setLoading(false); + } + }; + + // ============================== fetchAppAdminTokens ============================== + const fetchAppAdminTokens = async () => { + try { + const res = await axios.get(`${process.env.REACT_APP_URL}/utils/getapplocationconfig/?applocationid=${appId}`); + if (res.data.status) { + const tokens = res.data.details.applocationadmins.map((a) => a.userfcmtokem); + setAdmintoken(tokens); + } + } catch (err) { + console.log(err); + } + }; + + useEffect(() => { if (appId) fetchAppAdminTokens(); }, [appId]); + + // ============================== sendnotifications ============================== + const sendnotifications = async () => { + try { + const res = await axios.post(`${process.env.REACT_APP_URL}/utils/sendnotifications`, { + priority: 'high', + registration_ids: admintoken, + data: { accessid: process.env.REACT_APP_RIDER_ACCESS_ID }, + notification: { title: 'Nearle Merchant', body: 'An Order has been placed successfully, kindly process the same', sound: 'ring' } + }); + if (res.data.message === 'Success') opentoast('Notification sent Successfully', 'success', 1000); + } catch (err) { + opentoast(err.message, 'error', 1000); + } + }; + + // ============================== CSV / XLSX upload ============================== + const cleanReceiverName = (name) => (typeof name === 'string' ? name.replace(/^[\d.\s]+/, '').trim() : name); + const normalizeHeader = (header) => header?.toString().trim().toLowerCase().replace(/\s+/g, ''); + const headerMap = { 'pickupdate(yyyy-mmm-dd)': 'date', 'sendername*': 'locationname', 'senderphone*': 'locationcontact', 'senderaddress*': 'locationaddress', 'receivername*': 'firstname', - 'receiverphone': 'contactno', + receiverphone: 'contactno', 'receiveralternatephone*': 'altcontactno', receiverfulladdress: 'address', receiverlatitude: 'latitude', @@ -711,34 +370,18 @@ const MultipleOrders = () => { ' Collect Cash': 'collectionamt' }; - // helper to normalize headers - const normalizeHeader = (header) => header?.toString().trim().toLowerCase().replace(/\s+/g, ''); - const handleFileDirectUpload = (event) => { try { const file = event.target.files?.[0]; - if (!file) { - opentoast('No file selected.', 'warning'); - return; - } - - const fileName = file.name.toLowerCase(); - const isCSV = fileName.endsWith('.csv'); - const isExcel = fileName.endsWith('.xls') || fileName.endsWith('.xlsx'); - - if (!isCSV && !isExcel) { - opentoast('Invalid file type. Please upload a CSV or Excel file.', 'warning'); - return; - } + if (!file) { opentoast('No file selected.', 'warning'); return; } + const fileNameLower = file.name.toLowerCase(); + const isCSV = fileNameLower.endsWith('.csv'); + const isExcel = fileNameLower.endsWith('.xls') || fileNameLower.endsWith('.xlsx'); + if (!isCSV && !isExcel) { opentoast('Invalid file type. Please upload a CSV or Excel file.', 'warning'); return; } const processData = (data, headers) => { - console.log('data', data); const normalizedMap = {}; - for (const key in headerMap) { - normalizedMap[normalizeHeader(key)] = headerMap[key]; - } - console.log('normalizedMap', normalizedMap); - + for (const key in headerMap) normalizedMap[normalizeHeader(key)] = headerMap[key]; const mappedData = data.map((row) => { const newRow = {}; for (const key in row) { @@ -746,95 +389,87 @@ const MultipleOrders = () => { const newKey = normalizedMap[cleanKey] || cleanKey; let value = row[key]; if (newKey === 'firstname') value = cleanReceiverName(value); - newRow[newKey] = value; } - return newRow; }); - - const missingCols = Object.keys(headerMap) - .filter((clientCol) => clientCol.endsWith('*')) - .filter((clientCol) => !headers.includes(normalizeHeader(clientCol))); - - if (missingCols.length > 0) { - isExcel && opentoast(`Missing columns: ${missingCols.join(', ')}`, 'warning'); + const requiredCols = Object.keys(headerMap).filter((k) => k.trim().endsWith('*')); + const missingRequired = requiredCols.filter((clientCol) => !headers.includes(normalizeHeader(clientCol))); + if (missingRequired.length > 0) { + opentoast(`Missing columns: ${missingRequired.join(', ')}`, 'warning', 3000); } - - console.log('✅ Final Processed Data:', mappedData); setUsers(mappedData); - opentoast('File uploaded and successfully ', 'success', 3000); - opentoast('Press Continue', 'warning', 3000); + opentoast('File uploaded successfully', 'success', 2000); + opentoast('Press Continue to add as drop customers', 'info', 2500); }; - // ============ CSV handler ============ if (isCSV) { Papa.parse(file, { - header: true, - dynamicTyping: true, - skipEmptyLines: true, + header: true, dynamicTyping: true, skipEmptyLines: true, complete: (results) => { - if (!results.data?.length) { - opentoast('CSV file is empty or has no valid rows.', 'warning'); - setUsers([]); - return; - } - const headers = results.meta.fields.map(normalizeHeader); - processData(results.data, headers); + if (!results.data?.length) { opentoast('CSV file is empty or has no valid rows.', 'warning'); setUsers([]); return; } + processData(results.data, results.meta.fields.map(normalizeHeader)); }, - error: (error) => { - console.error('❌ CSV Parsing Error:', error); - opentoast(`CSV parsing failed: ${error.message}`, 'warning'); - } + error: (error) => opentoast(`CSV parsing failed: ${error.message}`, 'warning') }); } - - // ============ Excel handler ============ if (isExcel) { const reader = new FileReader(); reader.onload = (e) => { try { - const data = e.target.result; - // Try reading as binary first - let workbook; - try { - workbook = XLSX.read(data, { type: 'binary' }); - } catch { - // fallback for modern XLSX files - const arrayBuffer = new Uint8Array(data); - workbook = XLSX.read(arrayBuffer, { type: 'array' }); - } - - const firstSheet = workbook.SheetNames[0]; - const worksheet = workbook.Sheets[firstSheet]; - const jsonData = XLSX.utils.sheet_to_json(worksheet, { defval: '' }); - - if (!jsonData?.length) { - opentoast('Excel file is empty or invalid.', 'warning'); - setUsers([]); - return; - } - - const headers = Object.keys(jsonData[0]).map(normalizeHeader); - processData(jsonData, headers); + const workbook = XLSX.read(e.target.result, { type: 'binary' }); + const jsonData = XLSX.utils.sheet_to_json(workbook.Sheets[workbook.SheetNames[0]], { defval: '' }); + if (!jsonData?.length) { opentoast('Excel file is empty or invalid.', 'warning'); setUsers([]); return; } + processData(jsonData, Object.keys(jsonData[0]).map(normalizeHeader)); } catch (err) { - console.error('❌ Error processing Excel:', err); opentoast(`Error reading Excel: ${err.message}`, 'warning'); } }; - - // Important: use readAsBinaryString for Excel reader.readAsBinaryString(file); } } catch (err) { - console.error('Unexpected error during file upload:', err); opentoast(`Unexpected error: ${err.message}`, 'warning'); } }; - // =============================================== || createorders || =============================================== + const removeFileExtension = (n) => n.replace(/\.[^/.]+$/, ''); + const onFileChange = (event) => { + const file = event.target.files[0]; + if (!file) return; + const cleanedName = removeFileExtension(file.name); + setFileName((prev) => (prev ? `${prev}, ${cleanedName}` : cleanedName)); + handleFileDirectUpload(event); + }; + + // ============================== row editors ============================== + const handleQuantityChange = (customerid, value) => { + setDropCust((prev) => prev.map((c) => (c.customerid === customerid ? { ...c, quantity: Number(value) || 0 } : c))); + }; + const handleCollectionAmtChange = (customerid, value) => { + setDropCust((prev) => prev.map((c) => (c.customerid === customerid ? { ...c, collectionamt: Number(value) || 0 } : c))); + }; + + // ============================== buildDeliveryTime ============================== + const buildDeliveryTime = () => { + if (pickupSlot) { + const parsed = dayjs(pickupSlot, ['YYYY-MM-DD hh:mm A', 'YYYY-MM-DD HH:mm:ss']); + if (parsed.isValid()) return parsed.format('YYYY-MM-DD HH:mm:ss'); + } + if (startdate && selectedtime) { + const parsed = dayjs(`${dayjs(startdate).format('YYYY-MM-DD')} ${selectedtime}`, ['YYYY-MM-DD hh:mm A', 'YYYY-MM-DD HH:mm:ss']); + if (parsed.isValid()) return parsed.format('YYYY-MM-DD HH:mm:ss'); + } + return dayjs().format('YYYY-MM-DD HH:mm:ss'); + }; + + // ============================== createorders ============================== const createorders = async () => { - // ===================== Build Payload ===================== + if (!tenantid) { opentoast('Client not found. Please re-login.', 'warning'); return; } + if (!pickCust) { opentoast('Pickup location required', 'warning'); return; } + if (!pickupSlot) { opentoast('Select a pickup slot', 'warning'); return; } + if (!dropCust.length) { opentoast('Add at least one drop customer', 'warning'); return; } + + const deliverytime = buildDeliveryTime(); const arr = dropCust.map((customer) => ({ applocationid: pickCust.applocationid, configid: 9, @@ -852,7 +487,6 @@ const MultipleOrders = () => { pickuplocationid: pickCust.locationid || 0, pickuplong: pickCust.longitude, tenantid: pickCust.tenantid, - customerid: +customer?.customerid, deliveryaddress: customer.address || '', deliverycharge: +customer.totalcharge || 0, @@ -865,16 +499,14 @@ const MultipleOrders = () => { deliverylocation: customer.suburb || '', deliverylocationid: customer.deliverylocationid || 0, deliverylong: customer.longitude?.toString() || '', - deliverytime: `${dayjs(startdate).format('YYYY-MM-DD')} ${dayjs(selectedtime.$d).format('HH:mm:ss')}`, + deliverytime, deliverytype: 'B', - itemcount: 1, quantity: customer.quantity, collectionamt: customer.collectionamt, kms: customer.distance?.toString() || '0', locationid: +pickCust.locationid, moduleid: +pickCust.moduleid, - orderamount: +customer.totalcharge || 0, ordercharges: 0.0, orderdate: dayjs().format('YYYY-MM-DD HH:mm:ss'), @@ -884,764 +516,602 @@ const MultipleOrders = () => { pickupSlot })); - console.log('arr', arr); - - // ===================== Validation ===================== - if (!tenantid) { - opentoast('Choose Client', 'warning'); - return; - } setLoading(true); + setBtnLoading(true); try { const res = await axios.post(`${process.env.REACT_APP_URL}/orders/createorders`, arr); if (res.data.status) { - opentoast('Order Created Successfully', 'success', 2000); - if (admintoken) { - sendnotifications(); - } + opentoast('Orders Created Successfully', 'success', 2000); + if (admintoken) sendnotifications(); navigate('/nearle/orders'); - setLoading(false); } else { - console.log(res.data); - console.error('Create order failed (API response):', res.data); opentoast(res?.data?.message || 'Order creation failed. Please try again.', 'warning', 3000); } } catch (err) { - opentoast(err.message, 'error', 2000); - console.log('create orders', err.message); - console.error('Create order error:', { - message: err.message, - response: err.response, - request: err.request, - stack: err.stack - }); - - // Exact but short error for user - let toastMessage = 'Something went wrong. Please try again.'; - - if (err.response) { - // Server responded with error - toastMessage = err.response.data?.message || `Server error (${err.response.status})`; - } else if (err.request) { - // No response received - toastMessage = 'Network error. Check your internet connection.'; - } - opentoast(toastMessage, 'error'); - setLoading(false); + const msg = err.response ? err.response.data?.message || `Server error (${err.response.status})` : err.request ? 'Network error. Check your internet connection.' : 'Something went wrong.'; + opentoast(msg, 'error', 3000); } finally { setLoading(false); setBtnLoading(false); } }; - const [fileName, setFileName] = useState(''); - const removeFileExtension = (fileName) => { - return fileName.replace(/\.[^/.]+$/, ''); - }; - - const onFileChange = (event) => { - const file = event.target.files[0]; - if (!file) return; - const cleanedName = removeFileExtension(file.name); - setFileName((prev) => (prev ? `${prev}, ${cleanedName}` : cleanedName)); - if (tenantid === 916) { - handleFileDirectUpload(event); - } else { - handleFileDirectUpload(event); - // handleFileUpload(event); - } - }; - - const handleQuantityChange = (customerid, value) => { - setDropCust((prev) => prev.map((cust) => (cust.customerid === customerid ? { ...cust, quantity: Number(value) || 0 } : cust))); - }; - const handleCollectionAmtChange = (customerid, value) => { - setDropCust((prev) => prev.map((cust) => (cust.customerid === customerid ? { ...cust, collectionamt: Number(value) || 0 } : cust))); - }; + // ============================== derived ============================== + const canSubmit = !!locationid && !!pickupSlot && dropCust.length > 0; + const prereqOk = true; + const previewMode = dropCust.length > 0 ? 'drops' : users.length > 0 ? 'preview' : 'empty'; + // ============================== render ============================== return ( <> - {loading && ( - <> - - {/* */} - - )} - { - theme.zIndex.drawer + 1 - }} - open={btnLoading} // when loader = true, backdrop covers the page - > - - - } + {loading && } + t.zIndex.drawer + 1 }} open={btnLoading}> + + - - - - Create Multiple Order - - - - - - {/* ===================================================== || Choose App location || ===================================================== */} + {/* Title bar */} + + + Create Multiple Orders + + + + + Bulk-create deliveries from CSV/Excel or saved customers. + + - {/* ===================================================== ||Business Location || ===================================================== */} - - {tenantLocations?.length == 1 ? ( - - - - ) - }} - /> - ) : ( - `${option.locationname} (${option.suburb})` || ''} - value={locationValue} - onOpen={(event) => { - if (!appId && !tenantid) { - event.preventDefault(); - OpenToast('Please select Location and Tenant first!', 'warning', 3000); - setTimeout(() => { - locationRef.current?.focus(); - }, 0); - } else if (!tenantid) { - event.preventDefault(); - OpenToast('Please select Tenant first!', 'warning', 3000); - setTimeout(() => { - tenantRef.current?.focus(); - }, 0); - } - }} - onChange={(event, value, reason) => { - if (reason === 'clear') { - setLocationid(0); - setLocationValue(null); - setPickCust(null); - } else { - setLocationid(value.locationid || 0); - setLocationValue(value); - setPickCust(value); - setPickupSlotsList(value?.slots); - } - }} - renderInput={(params) => } - /> - )} - - - - - {/* ===================================================== || Pickup || ===================================================== */} - - - {locationid !== 0 && ( - - - - - - )} - {/* ================================================= || Time || ================================================= */} + {/* 50 / 50 workspace */} + - - - - - { - setStartdate(e); - let dateres11 = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd'); - console.log('dateres11'); - console.log(dateres11); - setSelectedtime(''); - if (dateres11 <= 0) { - console.log('startdate', e); - setStartdate(e); - - let arr = []; - timeslotarr.map((val) => { - if (dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0) { - arr.push(val); - } - }); + {/* LEFT — input fields */} + + {/* Card: Schedule & Pickup */} + + + + Schedule & Pickup + + + + + {tenantLocations?.length === 1 ? ( + + + + ) + }} + sx={{ '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px' } }} + /> + ) : ( + option ? `${option.locationname} (${option.suburb || option.locationsuburb || ''})` : ''} + value={locationValue} + onChange={(event, value, reason) => { + if (reason === 'clear' || !value) { + setLocationid(0); + setLocationValue(null); + setPickCust(null); + setPickupSlotsList(null); } else { - setAlertmessage('choose Upcoming Date'); - opentoast('choose Upcoming Date', 'warning'); - setStartdate(NaN); + setLocationid(value.locationid || 0); + setLocationValue(value); + setPickCust(value); + setPickupSlotsList(value?.slots || null); + } + }} + renderInput={(params) => } + sx={{ '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px' } }} + /> + )} + + + + { + if (!e || !dayjs(e).isValid()) { setStartdate(dayjs().format('MM-DD-YYYY')); return; } + const diffDays = dayjs().diff(dayjs(dayjs(e).format('YYYY-MM-DD')), 'd'); + if (diffDays <= 0) { + setStartdate(dayjs(e).format('MM-DD-YYYY')); + setSelectedtime(''); + setPickupSlot(null); + } else { + opentoast('Choose an upcoming date', 'warning'); + setStartdate(dayjs().format('MM-DD-YYYY')); } }} - value={dayjs(startdate)} - sx={{ width: 'auto', mt: 0 }} disablePast + slotProps={{ + textField: { + size: 'small', fullWidth: true, InputLabelProps: { shrink: true }, + InputProps: { + startAdornment: ( + + + + ) + }, + sx: { '& .MuiOutlinedInput-root': { borderRadius: '10px', height: '36px', paddingLeft: '10px' } } + } + }} /> - - {/* {timeslotarr.length > 0 && ( - - - - - - - Time - - - - - - { - setStartdate(e); - let dateres11 = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd'); - console.log('dateres11'); - console.log(dateres11); - setSelectedtime(''); - if (dateres11 <= 0) { - console.log('startdate', e); - setStartdate(e); - - let arr = []; - timeslotarr.map((val) => { - if ( - dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0 - ) { - arr.push(val); - } - }); - } else { - setAlertmessage('choose Upcoming Date'); - opentoast('choose Upcoming Date', 'warning'); - setStartdate(NaN); - } - }} - value={dayjs(startdate)} - sx={{ width: 'auto', mt: 0 }} - disablePast - /> - - - - - - - {timeslotarr.map((val, index) => { - if ( - dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0 - ) { - return ( - - - { - console.log('selectedtime', val); - setSelectedtime(val); - }} - // onClick={() => { - // if (distance > appLocaRadius) { - // setOpen(true); - // } else if (showDistance) { - // console.log('selectedtime', val); - // setSelectedtime(val); - // } else { - // opentoast('Out of city limit', 'error'); - // } - // }} - /> - - - ); - } - })} - - - - - - )} */} - - - { - if (reason === 'clear') { - setSelectedtime(null); - setPickupSlot(null); - } else { - // Convert to AM/PM and merge with date + + + { + if (reason === 'clear' || !newValue) { setSelectedtime(null); setPickupSlot(null); return; } + if (!newValue.time) { OpenToast('This slot has no time configured.', 'warning', 3000); return; } const formattedTime = dayjs(newValue.time, 'HH:mm').format('hh:mm A'); setSelectedtime(formattedTime); const finalDateTime = dayjs(`${startdate} ${formattedTime}`, 'MM-DD-YYYY hh:mm A').format('YYYY-MM-DD hh:mm A'); setPickupSlot(finalDateTime); - } - }} - getOptionLabel={(option) => `${option.name} (${dayjs(option.time, 'HH:mm').format('hh:mm A')})`} - renderInput={(params) => } - /> + }} + getOptionLabel={(option) => option ? `${option.name} (${dayjs(option.time, 'HH:mm').format('hh:mm A')})` : ''} + renderInput={(params) => ( + + + {params.InputProps.startAdornment} + + ) + }} + /> + )} + /> + + + {pickCust ? ( + + + + + + + {pickCust.locationname || '—'} + + + {pickCust.address || '—'} + + + + ) : ( + + Pickup auto-fills once a Business Location is selected. + + )} + - + + + {/* Card: Notes */} + + + Order Notes + Applied to every order + + + + + setOtherinstructions(e.target.value)} + sx={{ + '& .MuiOutlinedInput-root': { borderRadius: '10px', padding: '0 10px', alignItems: 'center', fontSize: '12px', background: '#ffffff', height: '32px' }, + '& .MuiOutlinedInput-input': { padding: '0 !important', fontSize: '12px !important', lineHeight: '32px' } + }} + /> + + + + + {/* Card: Summary + Submit */} + + + Bulk Summary + Live totals + + {(() => { + const metric = ({ icon: Icon, label, value, accent, active }) => ( + + + + + + {label} + {value} + + + ); + return ( + + {metric({ icon: FaRoute, label: 'Distance', value: totaldist ? `${totaldist} km` : '—', accent: '#1890ff', active: !!totaldist })} + {metric({ icon: FaBoxes, label: 'Quantity', value: totalQty || 0, accent: '#16a34a', active: !!totalQty })} + {metric({ icon: FaMoneyBillWave, label: 'Cash Collect', value: `₹${Number(totalCash).toFixed(2)}`, accent: '#d97706', active: !!totalCash })} + {metric({ icon: FaTruck, label: 'Deliveries', value: dropCust.length, accent: '#65387a', active: dropCust.length > 0 })} + + ); + })()} + + {dropCust.length > 0 && ( +
+
+ +
Total Charge
+
+
₹{Number(totalAmt).toFixed(2)}
+
+ )} + + + + +
+
+ + {/* RIGHT — file / drop preview */} + + + {/* Preview header */} + + + + {previewMode === 'drops' ? 'Drop List' : previewMode === 'preview' ? 'File Preview' : 'Preview'} + + + + {previewMode === 'drops' && ( + + )} + {previewMode === 'preview' && ( + } + sx={{ height: 22, fontWeight: 700, fontSize: 11, bgcolor: 'rgba(245,158,11,0.12)', color: '#b45309', border: '1px solid rgba(245,158,11,0.30)', '& .MuiChip-icon': { color: '#b45309' } }} + /> + )} + {previewMode !== 'empty' && (() => { + const handleHeaderPick = (val) => { + if (!prereqOk) { OpenToast('Please select a Business Location first.', 'warning', 3000); return; } + setUploadType(val); + if (val === 0) document.getElementById('upload-file')?.click(); + else if (val === 1) { setIsCustomerOpen(true); setSearchCustList(''); } + }; + return ( + + + + + ); + })()} + + + {fileName && ( + + + {fileName} + + )} + + {previewMode === 'preview' && users.length >= 1 && ( + + + Process & Calculate Distances + Click continue to import spreadsheet rows and calculate drop charges. + + + + )} + + {/* Scrollable preview body */} + + + {/* Drops — table */} + {previewMode === 'drops' && ( + + + + + # + Customer + Address + Qty + Cash + Km + Charge + + + + + {dropCust.map((customer, index) => ( + + {index + 1} + {customer.firstname} + + + + {customer.address} + + + + + {uploadType === 0 ? customer.quantity : ( + handleQuantityChange(customer.customerid, e.target.value)} + inputProps={{ min: 0 }} + sx={{ width: 64, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }} + /> + )} + + + {uploadType === 0 ? `₹${Number(customer.collectionamt || 0).toFixed(2)}` : ( + { const v = Number(e.target.value); handleCollectionAmtChange(customer.customerid, v > 0 ? v : 0); }} + inputProps={{ min: 0 }} + InputProps={{ startAdornment: }} + sx={{ width: 90, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }} + /> + )} + + {customer.distance} + ₹{Number(customer?.totalcharge || 0).toFixed(2)} + + + handleCheckboxChange1(customer)} sx={{ color: '#ef4444', p: 0.5 }}> + + + + + + ))} + + Total + + {totalQty} + ₹{Number(totalCash).toFixed(2)} + {totaldist} + ₹{Number(totalAmt).toFixed(2)} + + + +
+
+ )} + + {/* File preview — table */} + {previewMode === 'preview' && ( + + + + + # + Name + Contact + Address + Qty + Cash + + + + {users.map((u, i) => ( + + {i + 1} + {u.firstname || '—'} + {u.contactno || '—'} + + + + {u.address || '—'} + + + + {u.quantity ?? '—'} + {u.collectionamt != null ? `₹${Number(u.collectionamt).toFixed(2)}` : '—'} + + ))} + +
+
+ )} + + {/* Empty state */} + {previewMode === 'empty' && (() => { + const handleEmptyPick = (val) => { + if (!prereqOk) { OpenToast('Please select a Business Location first.', 'warning', 3000); return; } + setUploadType(val); + setDropCust([]); setUsers([]); setFileName(''); + if (val === 0) document.getElementById('upload-file')?.click(); + else if (val === 1) { setIsCustomerOpen(true); setSearchCustList(''); } + }; + const tile = ({ value, icon: Icon, title, sub, accent }) => ( + handleEmptyPick(value)} + onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleEmptyPick(value); } }} + sx={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', textAlign: 'left', gap: 1.25, px: 1.5, py: 1.25, borderRadius: '10px', border: '1.5px solid #eef2f6', bgcolor: '#fff', opacity: prereqOk ? 1 : 0.6, cursor: prereqOk ? 'pointer' : 'not-allowed', transition: 'all 0.18s ease', '&:hover': prereqOk ? { borderColor: accent, boxShadow: `0 4px 12px -4px ${accent}40`, transform: 'translateY(-1px)' } : undefined }} + > + + + + + {title} + {sub} + + + ); + return ( + + + + + Choose a Drop Source to begin + + Select a source below to import or pick your delivery customers. + + + {tile({ value: 0, icon: MdOutlineCloudUpload, title: 'Excel / CSV', sub: 'Bulk upload a sheet', accent: '#1890ff' })} + {tile({ value: 1, icon: FaUsers, title: 'Selection', sub: 'Pick saved customers', accent: '#65387a' })} + + + ); + })()} +
+
+
- {/* ===================================================== || Drop || ===================================================== */} + - - - - Drop ({dropCust?.length || 0}) - - - {/* ================= Upload CSV ================= */} - {uploadType === 0 && ( - <> - {fileName && ( - - - - {fileName} - - - )} - - - - - - )} - - {/* ================= Continue ================= */} - {users.length >= 1 && uploadType === 0 && ( - - )} - - {/* ================= Select Customers ================= */} - {uploadType === 1 && ( - - )} - - {/* ================= Upload Type ================= */} - - - Upload Type - { - if (!locationid) { - OpenToast('Please select Business Location !', 'warning', 3000); - return; - } - setUploadType(Number(e.target.value)); - setDropCust([]); - setUsers([]); - setFileName(''); - }} - > - } label="Excel / CSV" /> - } label="Selection" /> - - - - - - } - > - - - {dropCust?.length > 0 ? ( - <> - - - S.No - Customer - Address - Quantity - - - Cash Collect - - Kms - - Charge - Action - - - - - {dropCust?.map((customer, index) => ( - - {index + 1} - {customer.firstname} - {customer.address} - - {uploadType == 0 ? ( - {customer.quantity} - ) : ( - handleQuantityChange(customer.customerid, e.target.value)} - inputProps={{ min: 0 }} - /> - )} - - - {uploadType == 0 ? ( - ₹{Number(customer.collectionamt || 0).toFixed(2)} - ) : ( - { - if (e.target.value <= 0) { - handleCollectionAmtChange(customer.customerid, 0); - } else { - handleCollectionAmtChange(customer.customerid, e.target.value); - } - }} - inputProps={{ min: 0 }} - InputProps={{ - startAdornment: - }} - /> - )} - - - {customer.distance} - {`₹${customer?.totalcharge?.toFixed(2)}`} - - { - <> - handleCheckboxChange(event, customer)} - onClick={() => handleCheckboxChange1(customer)} - /> - - } - - - ))} - {dropCust?.length != 0 && ( - - Total - - - - {`${totalQty} `} - - - - {`${totalCash?.toFixed(2)} `} - - - {`${totaldist} `} - - - {`₹${totalAmt?.toFixed(2)}`} - - - - - )} - - - ) : ( - - {/* Header */} - - {' '} - - Important Instructions - - - - {/* Ordered List */} - - - Choose either Upload Type to upload CSV/Excel files, or - Selection Type to select from saved customers. - - - - Uploaded CSV or Excel files must follow the required format and contain the correct column names. - - - - Multiple files can be uploaded, but only one file at a time. - - - - Invalid or incorrectly formatted files will not be processed. - - - - )} -
-
-
-
- - - {/* ================================================= || Notes || ================================================= */} - - - - setOtherinstructions(e.target.value)} - /> - - - - - - - - - {/* ============================================= || saved address Dialog || ============================================= */} + {/* Saved customers dialog */} { - setIsCustomerOpen(false); - }} - fullWidth - sx={{ minWidth: 'lg' }} + open={isCustomerOpen} onClose={() => setIsCustomerOpen(false)} + fullWidth fullScreen={isMobile} + sx={{ '& .MuiDialog-paper': { borderRadius: { xs: 0, sm: '16px' }, overflow: 'hidden' } }} > - - - {`Select Drop Customers (${dropCust.length || 0})`} - - - setSearchCustList(e.target.value)} - sx={{ - '& .MuiOutlinedInput-input': { - p: '10.5px 0px 12px' - }, - bgcolor: 'white' - }} - startAdornment={ - - - - } - endAdornment={ - { - setSearchCustList(''); - }} - > - - - } - autoComplete="off" - /> - + + + + {`Select Drop Customers (${dropCust.length || 0})`} + + + setSearchCustList(e.target.value)} + sx={{ bgcolor: 'white', borderRadius: '10px', '& .MuiOutlinedInput-input': { p: '10px 14px' } }} + startAdornment={} + endAdornment={ setSearchCustList('')}>} + autoComplete="off" + /> - - {customerlist?.length == 0 ? ( - - + + {customerlist?.length === 0 ? ( + + ) : ( - - {customerlist && - customerlist?.map((customer, index) => ( - + + {customerlist?.map((customer, index) => { + const checked = dropCust.some((c) => c.customerid === customer.customerid); + return ( + cust.customerid === customer.customerid)} // Set the checked state of the checkbox based on whether the customer is in `dropCust` - onChange={(event) => handleCheckboxChange(event, customer)} - /> - } + sx={{ m: 0, py: 1, px: 1.25, borderRadius: '10px', '&:hover': { bgcolor: 'rgba(24,144,255,0.04)' } }} + control={ handleCheckboxChange(event, customer)} />} label={ -
- - {`${customer.firstname} (${customer.contactno})`} + + + {customer.firstname} ({customer.contactno}) - - - {customer.address} - -
+ {customer.address} +
} /> - ))} + ); + })} )} - + diff --git a/src/pages/nearle/orders/orders.js b/src/pages/nearle/orders/orders.js index c342d51..9ea7eb5 100644 --- a/src/pages/nearle/orders/orders.js +++ b/src/pages/nearle/orders/orders.js @@ -5,7 +5,6 @@ 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, @@ -25,11 +24,15 @@ import { DialogContent, Tooltip, Skeleton, - Autocomplete, - TextField, CircularProgress, InputBase, - InputAdornment + Backdrop, + SpeedDial, + SpeedDialIcon, + SpeedDialAction, + Badge, + TableContainer, + Checkbox } from '@mui/material'; import { MdAccessTime, @@ -42,22 +45,28 @@ import { MdHourglassEmpty, MdInventory2, MdLocalShipping, - MdLocationOn, - MdMyLocation, - MdNote, - MdPlace, MdSearch, MdStraighten, MdCalendarMonth, MdReceiptLong, - MdClear + MdClear, + MdNotes } from 'react-icons/md'; -import TableContainer from '@mui/material/TableContainer'; +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 { useInfiniteQuery } from '@tanstack/react-query'; +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. @@ -82,48 +91,14 @@ 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'; -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, @@ -145,13 +120,9 @@ const ROW_STATUS_META = { // 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' } + { idx: 0, status: 'created', label: 'Created', color: BRAND, icon: MdLocalShipping, countKey: 'created' } ]; -// Filled status badge — high-contrast pill (white text on solid color). const StatusBadge = ({ status }) => { const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || { label: status || '—', @@ -219,35 +190,20 @@ const MetricCell = ({ value, color, icon, isMoney = false }) => { }; const Orders = () => { + const navigate = useNavigate(); + const location = useLocation(); 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 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')); @@ -255,6 +211,13 @@ const Orders = () => { 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); @@ -262,13 +225,6 @@ const Orders = () => { return () => clearTimeout(handler); }, [searchword]); - const tabCounts = { - created: createdLenght, - pending: pendingLenght, - delivered: deliveredlenght, - cancelled: cancelledLenght - }; - const handleChangetab = (e, i) => { setSearchword(''); setRowsPerPage(10); @@ -282,8 +238,39 @@ const Orders = () => { 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 () => { - setLoading(true); await axios .put(`${process.env.REACT_APP_URL}/orders/updateorder`, { orderheaderid: orderheaderid, @@ -298,14 +285,13 @@ const Orders = () => { autoHideDuration: 2000 }); refetchOrders(); - fetchorderscount(); + orderscountRefetch(); + percentagedataRefetch(); setCancelOpen(false); } - setLoading(false); }) .catch((err) => { console.log(err); - setLoading(false); }); }; @@ -329,7 +315,8 @@ const Orders = () => { } = useInfiniteQuery({ queryKey: [tabstatus, startdate, enddate, page, rowsPerPage, debouncedSearch, locationId], queryFn: fetchOrders, - getNextPageParam: (lastPage) => lastPage.nextPage + getNextPageParam: (lastPage) => lastPage.nextPage, + refetchInterval: 15000 }); const rows = rowdata ? rowdata.pages.flatMap((p) => p.data) : []; @@ -343,8 +330,6 @@ const Orders = () => { } }, { - // 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 @@ -365,96 +350,129 @@ const Orders = () => { } }; - 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(); - }, []); + // ==============================|| Mutations ||============================== // - 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); + const createDeliveryMutation = useMutation({ + mutationFn: createAutomationDeliveries, + onSuccess: (data, variables) => { + enqueueSnackbar('Orders Optimised Successfully', { variant: 'success', autoHideDuration: 2000 }); + 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); } - }; - 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); + 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: [] + }); }; - 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 } + { 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 ( - {loading && ( + {(fetchpercentageIsLoading || isLoadingGetOrders || isLoading || createloader) && ( <> )} + {rows.length > 0 && currentStatus === 'created' && ( + + )} + {/* ============================================= || Header (compact) || ============================================= */} { - {/* 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) || ============================================= */} { - {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 } }} - /> - )} @@ -807,7 +645,7 @@ const Orders = () => { {ORDERS_STATUS_TABS.map((t) => { const Icon = t.icon; const active = tabvalue === t.idx; - const count = tabCounts[t.countKey] ?? 0; + const count = ordersCountData?.[t.countKey] ?? 0; return ( { ); })} - - - - - 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 }} - > - - - )} - - + @@ -939,10 +729,6 @@ const Orders = () => { 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 }, @@ -987,11 +773,11 @@ const Orders = () => { - {(isLoadingGetOrders || loading) && + {(isLoadingGetOrders || createloader) && rows.length === 0 && Array.from({ length: 10 }).map((_, idx) => ( - {Array.from({ length: currentStatus === 'created' ? 11 : 10 }).map((__, ci) => ( + {Array.from({ length: currentStatus === 'created' ? 12 : 11 }).map((__, ci) => ( @@ -1001,7 +787,7 @@ const Orders = () => { {!isLoadingGetOrders && rows.length === 0 && ( - + { )} - {rows.map((row, index) => ( - - - - {page * rowsPerPage + index + 1} - - + {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 })) + ); + } + }; - - - {row.locationname} - {row.locationsuburb && ` - ${row.locationsuburb}`} - - - - {row.orderid} + return ( + + + + {page * rowsPerPage + index + 1} - - - - {(() => { - 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.locationname} + {row.locationsuburb && ` - ${row.locationsuburb}`} - - {row.pickupcontactno} - - - - {row.pickupsuburb || (row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—')} + + + {row.orderid} - - - - - - - {row.deliverycustomer} - - - {row.deliverycontactno} - - - - {row.deliverysuburb || - (row.deliveryaddress?.length > 20 ? `${row.deliveryaddress.slice(0, 20)}…` : row.deliveryaddress || '—')} - - - - - - - } /> - - - - } isMoney /> - - - - } /> - - - - } isMoney /> - - - - {row.ordernotes ? ( - - - - {row.ordernotes} - + + + {(() => { + 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 || '—')} + + + + + + + } /> + - {currentStatus === 'created' && ( - {row.orderstatus === 'created' && ( - - { - e.stopPropagation(); - setOrderheaderid(row.orderheaderid); - setCancelOpen(true); - }} + } 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 && ( - + { 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. + + + + + + + + + ); }; diff --git a/src/routes/MainRoutes.js b/src/routes/MainRoutes.js index 49760ca..7440539 100644 --- a/src/routes/MainRoutes.js +++ b/src/routes/MainRoutes.js @@ -22,6 +22,8 @@ const Customers = Loadable(lazy(() => import('pages/nearle/clients/customers'))) const Locations = Loadable(lazy(() => import('pages/nearle/locations/Locations'))); const Orders = Loadable(lazy(() => import('pages/nearle/orders/orders'))); +const Deliveries = Loadable(lazy(() => import('pages/nearle/deliveries/deliveries'))); +const OrdersPreview = Loadable(lazy(() => import('pages/nearle/orders/OrdersPreview'))); const Details = Loadable(lazy(() => import('pages/nearle/orders/details'))); const Accountsettings = Loadable(lazy(() => import('pages/nearle/accountsettings'))); @@ -63,11 +65,15 @@ const MainRoutes = { path: 'orders', children: [ { - path: '', // /orders + path: '', element: }, { - path: 'create/grouporders', // /orders/create/grouporders + path: 'preview', + element: + }, + { + path: 'create/grouporders', element: }, { @@ -76,6 +82,10 @@ const MainRoutes = { } ] }, + { + path: 'deliveries', + element: + }, { path: 'customers', children: [