From 61436440396ca019ec3d8f5c042d819414c2b38b Mon Sep 17 00:00:00 2001 From: Suriya Date: Wed, 8 Jul 2026 17:40:02 +0530 Subject: [PATCH] remove: invoice, reports (orders/riders summary+logs, profitability), legacy APIs Dropped entirely rather than adapted to Doormile, per direction: none of these are needed for the Doormile console right now. Deleted: - src/pages/nearle/invoice/ (invoice.js, invoicePreview.js) - src/pages/nearle/reports/ (ordersSummary, ordersDetails, ridersSummary, ridersLogs, profitability, plus their only consumers: mapWithRoute.js, RiderLocationMap.js, RidersRoutes.js) Removed the corresponding routes (MainRoutes.js) and sidebar entries (menu-items/nearle.js: the whole "reports" collapse + "invoice" item), and their locale keys from en.json. Removed now-orphaned api.js functions that only those pages called: getreportsummary, getreportlocationsummary, getriderbydelivery, fetchCount, fetchRidersSummary, fetchinvoiceinsight, fetchdeliverylist, fetchOrders1, getallriders. Verified each had zero remaining importers before removing. Kept fetchorderdetails (still used by orders/details.js) and fetchRidersLogs (still used by Dispatch.js) -- same name, different consumer than the deleted reports pages. --- src/menu-items/nearle.js | 58 - src/pages/api/api.js | 106 - src/pages/nearle/invoice/invoice.js | 859 -------- src/pages/nearle/invoice/invoicePreview.js | 480 ----- src/pages/nearle/reports/RiderLocationMap.js | 76 - src/pages/nearle/reports/RidersRoutes.js | 285 --- src/pages/nearle/reports/mapWithRoute.js | 204 -- src/pages/nearle/reports/ordersDetails.js | 1976 ------------------ src/pages/nearle/reports/ordersSummary.js | 1372 ------------ src/pages/nearle/reports/profitability.js | 799 ------- src/pages/nearle/reports/ridersLogs.js | 355 ---- src/pages/nearle/reports/ridersSummary.js | 1156 ---------- src/routes/MainRoutes.js | 51 - src/utils/locales/en.json | 7 - 14 files changed, 7784 deletions(-) delete mode 100644 src/pages/nearle/invoice/invoice.js delete mode 100644 src/pages/nearle/invoice/invoicePreview.js delete mode 100644 src/pages/nearle/reports/RiderLocationMap.js delete mode 100644 src/pages/nearle/reports/RidersRoutes.js delete mode 100644 src/pages/nearle/reports/mapWithRoute.js delete mode 100644 src/pages/nearle/reports/ordersDetails.js delete mode 100644 src/pages/nearle/reports/ordersSummary.js delete mode 100644 src/pages/nearle/reports/profitability.js delete mode 100644 src/pages/nearle/reports/ridersLogs.js delete mode 100644 src/pages/nearle/reports/ridersSummary.js diff --git a/src/menu-items/nearle.js b/src/menu-items/nearle.js index de980d9..05822ca 100644 --- a/src/menu-items/nearle.js +++ b/src/menu-items/nearle.js @@ -1,10 +1,8 @@ // third-party import { FormattedMessage } from 'react-intl'; import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined'; -import ReceiptOutlinedIcon from '@mui/icons-material/ReceiptOutlined'; import MopedOutlinedIcon from '@mui/icons-material/MopedOutlined'; import NearMeOutlinedIcon from '@mui/icons-material/NearMeOutlined'; -import { TbListDetails } from 'react-icons/tb'; // assets import { @@ -24,7 +22,6 @@ import { TeamOutlined, MailOutlined, ImportOutlined, - BarChartOutlined, MoneyCollectOutlined, FileDoneOutlined } from '@ant-design/icons'; @@ -47,8 +44,6 @@ const icons = { TeamOutlined, MailOutlined, ImportOutlined, - BarChartOutlined, - ReceiptOutlinedIcon, NearMeOutlinedIcon, DirectionsBikeOutlinedIcon, MopedOutlinedIcon, @@ -112,59 +107,6 @@ const nearle = { type: 'item', url: '/nearle/riders', icon: DirectionsBikeOutlinedIcon - }, - { - id: 'reports', - title: , - type: 'collapse', - icon: icons.BarChartOutlined, - children: [ - { - id: 'reports', - title: , - type: 'item', - url: '/nearle/reports/orderssummary', - icon: TbListDetails - }, - { - id: 'ordersdetails', - title: , - type: 'item', - url: '/nearle/reports/ordersdetails', - icon: icons.DashboardOutlined - // target: true - }, - { - id: 'riderssummary', - title: , - type: 'item', - url: '/nearle/reports/riderssummary', - icon: DirectionsBikeOutlinedIcon - // target: true - }, - { - id: 'riderslogs', - title: , - type: 'item', - url: '/nearle/reports/riderslogs', - icon: DirectionsBikeOutlinedIcon - // target: true - }, - { - id: 'profitability', - title: , - type: 'item', - url: '/nearle/reports/profitability', - icon: icons.BarChartOutlined - } - ] - }, - { - id: 'invoice', - title: , - type: 'item', - url: '/nearle/invoice', - icon: icons.ReceiptOutlinedIcon } ] }; diff --git a/src/pages/api/api.js b/src/pages/api/api.js index 3b9e6e7..9c17ba1 100644 --- a/src/pages/api/api.js +++ b/src/pages/api/api.js @@ -592,29 +592,6 @@ export const getriderstatus = async () => { return response.data.data; }; -// ==============================|| getreportsummary (orders summary)||============================== // -export const getreportsummary = async ({ queryKey }) => { - console.log('queryKey for getreportsummary', queryKey); - const [appId, tenantid, locationid, startdate, enddate] = queryKey; - const response = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getreportsummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}` - ); - console.log('getreportsummary', response.data.details); - - return response.data.details; -}; - -// ==============================|| getreportlocationsummary (orders summary)||============================== // -export const getreportlocationsummary = async ({ queryKey }) => { - console.log('queryKey for getreportlocationsummary', queryKey); - const [appId, tenantid, locationid, startdate, enddate] = queryKey; - const response = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getreportlocationsummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}` - ); - console.log('getreportlocationsummary', response.data.details); - - return response.data.details; -}; // ==============================|| fetchorderdetails (orders detail)||============================== // export const fetchorderdetails = async ({ queryKey }) => { console.log('queryKey of fetchorderdetails', queryKey); @@ -635,49 +612,6 @@ export const fetchorderdetails = async ({ queryKey }) => { return detailsWithSNo; }; -// ==============================|| getriderbydelivery (orders detail)||============================== // - -export const getriderbydelivery = async (startdate, enddate, appId = 0, tenantid = 0, locationid = 0) => { - // const [, startdate, enddate] = queryKey; - - try { - const response = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getriderbydelivery/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}` - ); - return response.data.details || []; - } catch (err) { - console.log('getriderbydelivery', err.message); - return []; - } -}; - -// ==============================|| fetchCount (orders detail)||============================== // - -export const fetchCount = async ({ queryKey }) => { - console.log('queryKey of fetchCount', queryKey); - const [appId, startdate, enddate] = queryKey; - let url = - appId == 0 - ? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?fromdate=${startdate}&todate=${enddate}` - : `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}`; - - const response = await axios.get(url); - return response.data.details; -}; - -// ==============================|| fetchRidersSummary (riders summary)||============================== // - -export const fetchRidersSummary = async ({ queryKey }) => { - console.log('queryKey for fetchRidersSummary', queryKey); - const [, appId, startdate, enddate] = queryKey; - const response = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getridersummary/?applocationid=${appId}&fromdate=${startdate}&todate=${enddate}` - ); - console.log('fetchRidersSummary', response.data.details); - - return response.data.details; -}; - // ==============================|| fetchLocations (orders summary))||============================== // export const fetchLocations = async () => { const response = await axios.get(`${process.env.REACT_APP_URL}/partners/getpartners`); @@ -689,21 +623,6 @@ export const fetchLocations = async () => { return updatedLocations; }; -// ==============================|| fetchinvoiceinsight (Invoice)||============================== // - -export const fetchinvoiceinsight = async () => { - const insightResponse = await axios.get(`${process.env.REACT_APP_URL}/invoice/getinvoiceinsight`); - return insightResponse.data.details; -}; - -// ==============================|| fetchdeliverylist (Invoice)||============================== // - -export const fetchdeliverylist = async ({ queryKey }) => { - const [billStatus] = queryKey; - const deliveyResponse = await axios.get(`${process.env.REACT_APP_URL}/invoice/getallinvoice/?billstatus=${billStatus}`); - console.log('fetchdeliverylist', deliveyResponse.data.details); - return deliveyResponse.data.details; -}; // ==============================|| fetchRidersLogs (RiderLogs)||============================== // export const fetchRidersLogs = async ({ queryKey }) => { @@ -716,21 +635,6 @@ export const fetchRidersLogs = async ({ queryKey }) => { return riderLogsResponse.data.details; }; -// ==============================|| getorders (Locations)||============================== // -// fetchOrders.js - -export const fetchOrders1 = async ({ pageParam = 1, queryKey }) => { - const [, tenantid, locationid, status, startdate, enddate, searchword, rowsPerPage] = queryKey; - - const res = await axios.get( - `${process.env.REACT_APP_URL}/orders/tenant/getorders/?tenantid=${tenantid}&locationid=${locationid}&status=${status}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}` - ); - - return { - details: res.data.details, - nextPage: res.data.details.length === rowsPerPage ? pageParam + 1 : undefined - }; -}; // ==============================|| getusers (viewProfile)||============================== // export const getusers = async () => { @@ -741,13 +645,3 @@ export const getusers = async () => { console.log('getusers', err.message); } }; -// ==============================|| getallriders (order)||============================== // - -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); - } -}; diff --git a/src/pages/nearle/invoice/invoice.js b/src/pages/nearle/invoice/invoice.js deleted file mode 100644 index f1cbf4e..0000000 --- a/src/pages/nearle/invoice/invoice.js +++ /dev/null @@ -1,859 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { Outlet, useNavigate } from 'react-router-dom'; -import { useQuery } from '@tanstack/react-query'; -import dayjs from 'dayjs'; -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); - -import { - Avatar, - Box, - Divider, - Grid, - IconButton, - Paper, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TablePagination, - TableRow, - Tooltip, - Typography, - useMediaQuery -} from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { - MdReceiptLong, - MdDashboard, - MdHourglassEmpty, - MdReportProblem, - MdCheckCircle, - MdGroups, - MdEventNote, - MdCurrencyRupee, - MdVisibility, - MdInventory2, - MdOutlinePendingActions, - MdOutlineCheckCircle -} from 'react-icons/md'; - -import { fetchinvoiceinsight, fetchdeliverylist } from 'pages/api/api'; -import Loader from 'components/Loader'; -import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; -import PageHeader from 'components/nearle_components/PageHeader'; -import StatCard from 'components/nearle_components/StatCard'; -import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; - -// ============================================================================ -// Design tokens — shared with deliveries / tenants / customers / pricing / -// orders-details / riders-summary pages. -// ============================================================================ -const DT = { - radiusPill: 999, - radiusCard: 14, - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', - shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)', - shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const a = (c, suffix) => `${c}${suffix}`; -const tint = (c) => a(c, '08'); -const soft = (c) => a(c, '18'); -const ring = (c) => a(c, '26'); -const edge = (c) => a(c, '55'); - -const BRAND = '#C01227'; - -const AccentAvatar = ({ color, selected, size = 24, children }) => ( - - {children} - -); - -// Bill status → tab visual meta (semantic colours; brand purple reserved for "All"). -const STATUS_META = { - 0: { key: 'all', label: 'All', color: BRAND, icon: MdDashboard, countKey: 'totalcount' }, - 1: { key: 'open', label: 'Open', color: '#ef4444', icon: MdHourglassEmpty, countKey: 'pendingcount' }, - 2: { key: 'overdue', label: 'Overdue', color: '#f59e0b', icon: MdReportProblem, countKey: 'overduecount' }, - 3: { key: 'paid', label: 'Paid', color: '#10b981', icon: MdCheckCircle, countKey: 'paidcount' } -}; -const STATUS_TABS = [0, 1, 2, 3]; - -function formatNumberToRupees(value) { - return new Intl.NumberFormat('en-IN', { - style: 'currency', - currency: 'INR', - minimumFractionDigits: 2 - }).format(Number(value) || 0); -} - -const Invoice = () => { - const navigate = useNavigate(); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const [billStatus, setBillStatus] = useState(0); - const [isloader, setIsLoader] = useState(false); - const [searchword, setSearchword] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - - const handleDebouncedSearch = React.useCallback((val) => { - setDebouncedSearch(val); - setPage(0); - }, []); - - // ============================================= || fetchinvoiceinsight || - const { - data: insightdata, - isLoading: isInsightLoading, - isError: isInsightError, - error: insightError - } = useQuery({ - queryKey: ['invoiceInsight'], - queryFn: fetchinvoiceinsight, - refetchInterval: 300000 - }); - - // ============================================= || fetchdeliverylist || - // NOTE: queryKey shape MUST stay `[billStatus]` — `fetchdeliverylist` - // destructures `const [billStatus] = queryKey`. - const { - data: deliveryList, - isLoading: isDeliveryLoading, - isError: isDeliveryError, - error: deliveryError - } = useQuery({ - queryKey: [billStatus], - queryFn: fetchdeliverylist, - refetchInterval: 300000 - }); - - const isLoading = isInsightLoading || isDeliveryLoading; - const isError = isInsightError || isDeliveryError; - const errorMessage = insightError?.message || deliveryError?.message; - - // Client-side filter across tenant name, contact person, invoice number. - const filteredList = useMemo(() => { - if (!deliveryList) return []; - if (!debouncedSearch) return deliveryList; - const q = debouncedSearch.toLowerCase().trim(); - return deliveryList.filter((row) => - [row.tenantname, row.contactperson, String(row.invoiceno)] - .filter(Boolean) - .some((field) => String(field).toLowerCase().includes(q)) - ); - }, [deliveryList, debouncedSearch]); - - const activePage = useMemo(() => { - const maxPage = Math.max(0, Math.ceil(filteredList.length / rowsPerPage) - 1); - return Math.min(page, maxPage); - }, [filteredList.length, page, rowsPerPage]); - - // Keep page state in sync when filters or data updates shrink the list below current page - React.useEffect(() => { - if (page !== activePage) { - setPage(activePage); - } - }, [page, activePage]); - - const pagedList = useMemo( - () => filteredList.slice(activePage * rowsPerPage, activePage * rowsPerPage + rowsPerPage), - [filteredList, activePage, rowsPerPage] - ); - - const grandTotal = useMemo( - () => filteredList.reduce((sum, r) => sum + (Number(r.totalamount) || 0), 0), - [filteredList] - ); - - const pageTotal = useMemo( - () => pagedList.reduce((sum, r) => sum + (Number(r.totalamount) || 0), 0), - [pagedList] - ); - - const handleChangePage = (event, newPage) => setPage(newPage); - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(+event?.target?.value); - setPage(0); - }; - - if (isError) { - return errorMessage; - } - - const KPI_META = [ - { idx: 0, label: 'All Invoices', color: BRAND, icon: MdDashboard, value: insightdata?.totalcount ?? 0 }, - { idx: 1, label: 'Open', color: '#ef4444', icon: MdOutlinePendingActions, value: insightdata?.pendingcount ?? 0 }, - { idx: 2, label: 'Overdue', color: '#f59e0b', icon: MdReportProblem, value: insightdata?.overduecount ?? 0 }, - { idx: 3, label: 'Paid', color: '#10b981', icon: MdOutlineCheckCircle, value: insightdata?.paidcount ?? 0 } - ]; - - const activeMeta = STATUS_META[billStatus]; - - return ( - <> - {(isloader || isLoading) && } - - {/* ============================================= || Header || ============================================= */} - - - - Grand Total - - - {formatNumberToRupees(grandTotal)} - - - } - /> - - {/* ============================================= || KPI Cards (clickable filter) || ============================================= */} - - {KPI_META.map((item) => { - const Icon = item.icon; - return ( - - { - setBillStatus(item.idx); - setPage(0); - }} - sx={{ cursor: 'pointer', height: '100%' }} - > - } - color={item.color} - loading={isInsightLoading} - /> - - - ); - })} - - - {/* ============================================= || Status Tabs + Search || ============================================= */} - - - - {STATUS_TABS.map((idx) => { - const meta = STATUS_META[idx]; - const Icon = meta.icon; - const active = billStatus === idx; - const count = insightdata?.[meta.countKey] ?? 0; - return ( - { - setBillStatus(idx); - setPage(0); - }} - 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: `1px solid ${active ? meta.color : DT.borderSubtle}`, - bgcolor: active ? meta.color : DT.surface, - color: active ? '#fff' : DT.textSecondary, - fontWeight: 600, - boxShadow: 'none', - transition: 'background-color 0.15s, border-color 0.15s, color 0.15s', - '&:hover': { - borderColor: active ? meta.color : '#cbd5e1', - bgcolor: active ? meta.color : DT.surfaceAlt - } - }} - > - - - - - {meta.label} - - - {count} - - - ); - })} - - - - - - - - - {/* ============================================= || Table || ============================================= */} - - {isMobile ? ( - <> - {isDeliveryLoading ? ( - - - - ) : pagedList.length === 0 ? ( - - - - - - No invoices to show - - - {searchword - ? 'Try a different keyword.' - : `No ${activeMeta.label.toLowerCase()} invoices for this filter.`} - - - ) : ( - - {pagedList.map((item, index) => { - const overdue = - billStatus === 2 || - (item.duedate && dayjs(item.duedate).isBefore(dayjs(), 'day') && billStatus !== 3); - return ( - - - - - - - - {item.tenantname || '—'} - - {item.contactperson && ( - - {item.contactperson} - - )} - - - - { - setIsLoader(true); - setTimeout(() => { - setIsLoader(false); - navigate('/nearle/invoice/preview', { state: item }); - }, 500); - }} - sx={{ - flexShrink: 0, - bgcolor: soft(BRAND), - color: BRAND, - border: `1px solid ${edge(BRAND)}`, - '&:hover': { bgcolor: BRAND, color: '#fff' } - }} - > - - - - - } - > - - - - {item.invoiceno || '—'} - - - - - - {formatNumberToRupees(item.totalamount).replace('₹', '').trim()} - - - - - - - {item.transactiondate ? dayjs(item.transactiondate).format('DD/MM/YYYY') : '—'} - - - - - - - - {item.duedate ? dayjs(item.duedate).format('DD/MM/YYYY') : '—'} - - - - - - {item.itemcount ?? 0} - - - - - ); - })} - - )} - - ) : ( - - - - - # - Client - Invoice ID - Invoice Date - Due Date - Items - Amount - Action - - - - - {isDeliveryLoading && } - {!isDeliveryLoading && pagedList.length === 0 ? ( - - - - - - - - No invoices to show - - - {searchword - ? 'Try a different keyword.' - : `No ${activeMeta.label.toLowerCase()} invoices for this filter.`} - - - - - ) : ( - pagedList.map((item, index) => { - const overdue = billStatus === 2 || (item.duedate && dayjs(item.duedate).isBefore(dayjs(), 'day') && billStatus !== 3); - return ( - - - - {String(activePage * rowsPerPage + index + 1).padStart(2, '0')} - - - - - - - - - - - {item.tenantname || '—'} - - {item.contactperson && ( - - {item.contactperson} - - )} - - - - - - - {item.invoiceno || '—'} - - - - - - - - - {item.transactiondate ? dayjs(item.transactiondate).format('DD/MM/YYYY') : '—'} - - - - {item.transactiondate ? dayjs(item.transactiondate).utc().format('hh:mm A') : ''} - - - - - - - - - - {item.duedate ? dayjs(item.duedate).format('DD/MM/YYYY') : '—'} - - - - {item.duedate ? dayjs(item.duedate).utc().format('hh:mm A') : ''} - - - - - - - {item.itemcount ?? 0} - - - - - - - {formatNumberToRupees(item.totalamount).replace('₹', '').trim()} - - - - - - { - setIsLoader(true); - setTimeout(() => { - setIsLoader(false); - navigate('/nearle/invoice/preview', { state: item }); - }, 500); - }} - sx={{ - bgcolor: soft(BRAND), - color: BRAND, - border: `1px solid ${edge(BRAND)}`, - '&:hover': { bgcolor: BRAND, color: '#fff' } - }} - > - - - - - - ); - }) - )} - -
-
- )} - - - - - Page total · {formatNumberToRupees(pageTotal)} - - - -
- - - - ); -}; - -export default Invoice; diff --git a/src/pages/nearle/invoice/invoicePreview.js b/src/pages/nearle/invoice/invoicePreview.js deleted file mode 100644 index 4b9371b..0000000 --- a/src/pages/nearle/invoice/invoicePreview.js +++ /dev/null @@ -1,480 +0,0 @@ -import React, { useRef, useState, useEffect } from 'react'; -import { useLocation } from 'react-router-dom'; -import { useTheme } from '@mui/material/styles'; -import useMediaQuery from '@mui/material/useMediaQuery'; -// import nearleLogo from '../../../assets/images/nearleLogo.png'; -import logo_nearle1 from '../../../assets/images/doormile-logo.png'; - -// doormile-logo.png is a white asset; this recolours it to brand red (#C01227) for the light invoice background. -const DOORMILE_RED_FILTER = 'brightness(0) saturate(100%) invert(15%) sepia(93%) saturate(5437%) hue-rotate(346deg) brightness(81%) contrast(92%)'; -import axios from 'axios'; -import dayjs from 'dayjs'; -import { enqueueSnackbar } from 'notistack'; -import { PrinterFilled } from '@ant-design/icons'; -import ReactToPrint from 'react-to-print'; -// import jsPDF from 'jspdf'; -import { useNavigate } from 'react-router-dom'; -import { FaArrowLeft } from 'react-icons/fa6'; -import { FaIndianRupeeSign } from 'react-icons/fa6'; - -// import autoTable from 'jspdf-autotable'; -import { - Grid, - Button, - Divider, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Typography, - Box, - IconButton, - TextField, - Tooltip, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Stack, - Chip -} from '@mui/material'; - -const InvoicePreview = () => { - const [selected, setselected] = useState({}); - const location = useLocation(); - const navigate = useNavigate(); - console.log('previewSelect', location.state); - const componentRef = useRef(null); - const [paydialog, setpaydialog] = useState(false); - const [refnumber, setRefnumber] = useState(''); - const [remarks, setRemarks] = useState(''); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - useEffect(() => { - setselected(location.state); - }, []); - - // ================================================= || formatNumberToRupees || ================================================= - - function formatNumberToRupees(value) { - return new Intl.NumberFormat('en-IN', { - style: 'currency', - currency: 'INR', - minimumFractionDigits: 2 - }).format(value); - } - - // ================================================= || updatePayment || ================================================= - - const updatePayment = async () => { - try { - const updateResponse = await axios.put(`${process.env.REACT_APP_URL}/invoice/updatestatus`, { - salesid: selected.salesid, - referenceno: refnumber, - referencedate: dayjs().format('YYYY-MM-DD HH:mm:ss'), - billstatus: 2, - paymentremarks: remarks - }); - if (updateResponse.status) { - enqueueSnackbar(' Updated Successfully ', { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 1000 - }); - } - console.log('updateResponse', updateResponse); - } catch (error) { - console.log('updateResponse', error); - } - }; - - return ( - <> - - - - { - navigate('/nearle/invoice'); - }} - > - - - - - - - Invoice Details - - - - - - - - ( - - )} - content={() => componentRef.current} - /> - - - - {/* minWidth keeps the invoice at a legible fixed layout on phones — - the parent's overflowX:auto then lets it scroll horizontally - instead of squishing the header into vertical slivers. 720px sits - within the print page width, so printing is unaffected. */} -
- - - - - - {' '} - - {/* - {`Invoice No: ${"\u00a0\u00a0\u00a0"}${selected.invoiceno}`} - */} - - - Invoice No : - - {`${'\u00a0\u00a0'}${selected.invoiceno}`} - - - - - - Date :{' '} - - - {dayjs(selected.transactiondate).format('DD-MM-YYYY')} - - - - - Due Date : - - {dayjs(selected.dueDate).format('DD-MM-YYYY')} - - {/* - - Invoice No : - - - {`${"\u00a0\u00a0\u00a0"}${selected.invoiceno}`} - - */} - - - - - - - - - - From: - - Nearle Technology Privite Limited. - - 424, 4thfloor, - - Red rose towers, - DB Road, RS Puram, - 641002. - care@nearle.in - 9047968666 - - - - - - - - - - - - To: - - {selected.tenantname} - {selected.address} - {selected.suburb} - {selected.city} - {selected.state}{' '} - - - - - - - - - - - - - - S.No - Particulars - Unit - Quantity - Rate - {/* {selected && selected.pricingtypeid === 73 && ( */} - Other Charges - {/* )} */} - Amount - - - {selected.tenantsalesdetails && ( - - - 1 - - - {`Invoice from ${dayjs(selected.tenantsalesdetails[0].fromdate).format('DD-MM-YYYY')} to ${dayjs( - selected.tenantsalesdetails[0].todate - ).format('DD-MM-YYYY')}`} - - - - {selected.tenantsalesdetails[0].pricingtype} - - - - {`${selected.tenantsalesdetails[0].quantity.toFixed(2)} km`} - - - {`₹ ${selected.tenantsalesdetails[0].baserate.toFixed(2)}`} - - {/* {selected.tenantsalesdetails[0].pricingtypeid == 73 && ( */} - - {`₹ ${selected.tenantsalesdetails[0].othercharges}.00`} - - {/* )} */} - - {`₹ ${selected.tenantsalesdetails[0].amount}.00`} - - - - )} -
-
- - - - - - - Sub Total: - {formatNumberToRupees(selected.salesamount)} - - - Discount: - - - {formatNumberToRupees(selected.discountamt)} - - - - Tax: - - + {formatNumberToRupees(selected.taxamount)} - - - - - Grand Total: - - {formatNumberToRupees(Math.round(selected.totalamount))} - - - - - -
- - - Notes: {selected.remarks} - - -
-
- {/* ================================================= || updatePayment Dialog || ================================================= */} - { - setpaydialog(false); - }} - maxWidth={'sm'} - fullWidth - > - - - - ₹ - - - Update Payment - - - - - - Reference No - { - setRefnumber(e.target.value); - }} - /> - - - Remarks - { - setRemarks(e.target.value); - }} - /> - - - - - - - - - ); -}; - -export default InvoicePreview; diff --git a/src/pages/nearle/reports/RiderLocationMap.js b/src/pages/nearle/reports/RiderLocationMap.js deleted file mode 100644 index 5dfabf2..0000000 --- a/src/pages/nearle/reports/RiderLocationMap.js +++ /dev/null @@ -1,76 +0,0 @@ -import { Button } from '@mui/material'; -import { LoadScriptNext, GoogleMap, Marker, OverlayView } from '@react-google-maps/api'; - -const containerStyle = { - width: '100%', - height: 'calc(100vh - 150px)' -}; - -export default function RiderLocationMap({ riderLocations }) { - console.log('riderLocations', riderLocations); - - const center = { - lat: Number(riderLocations?.[0]?.latitude || 11.0056), - lng: Number(riderLocations?.[0]?.longitude || 76.9661) - }; - const GreenIcon = { - url: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png', - scaledSize: new window.google.maps.Size(25, 41), - anchor: new window.google.maps.Point(12, 41) - }; - - const RedIcon = { - url: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png', - scaledSize: new window.google.maps.Size(25, 41), - anchor: new window.google.maps.Point(12, 41) - }; - - return ( - - - {riderLocations && - riderLocations?.map((r, index) => { - const lat = Number(r.latitude); - const lng = Number(r.longitude); - return ( -
- {/* Marker */} - - -
- -
-
-
- ); - })} -
-
- ); -} diff --git a/src/pages/nearle/reports/RidersRoutes.js b/src/pages/nearle/reports/RidersRoutes.js deleted file mode 100644 index 76e9983..0000000 --- a/src/pages/nearle/reports/RidersRoutes.js +++ /dev/null @@ -1,285 +0,0 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { GoogleMap, Polyline, Marker, InfoWindow, useJsApiLoader } from '@react-google-maps/api'; -import { Box, IconButton, Stack, Typography, CircularProgress } from '@mui/material'; -import { MdClose, MdRoute } from 'react-icons/md'; - -const containerStyle = { width: '100%', height: '100%' }; - -// Renders a single rider's PLANNED route for the date range chosen on the -// Riders Summary page. `details` is an ordered array of waypoints (sorted by -// the planning step number) shaped as: -// { step, orderid, deliveryid, customer, address, -// dropLat, dropLng, pickLat, pickLng, expectedTime } -// `dropLat/dropLng` are required; pickup coords are optional and rendered as -// faded pre-stops if present. -export default function RidersRoutes({ details, loading, riderName, dateRange, onClose }) { - const mapRef = useRef(null); - const [focusedStep, setFocusedStep] = useState(null); - const [routePath, setRoutePath] = useState([]); - const [routeLoading, setRouteLoading] = useState(false); - - const { isLoaded } = useJsApiLoader({ - googleMapsApiKey: process.env.REACT_APP_GOOGLE_MAPS_KEY - }); - - // Step-pin coordinates in planning order — what the polyline connects. - const dropPath = useMemo( - () => (details || []).map((d) => ({ lat: d.dropLat, lng: d.dropLng })), - [details] - ); - - // Auto-fit map bounds to the full planned path once the map and data are - // both ready. Re-runs whenever the route changes (different rider / date). - useEffect(() => { - if (!isLoaded || !mapRef.current || dropPath.length === 0) return; - const bounds = new window.google.maps.LatLngBounds(); - dropPath.forEach((p) => bounds.extend(p)); - mapRef.current.fitBounds(bounds, 48); - }, [isLoaded, dropPath]); - - // Resolve the rider's planned waypoints into an actual road-following path - // via the Directions API. Without this, the polyline would cut across - // buildings / aerial lines — operators have no way to read the real route. - // Directions has a 25-waypoint limit per request, so we chunk and stitch. - useEffect(() => { - if (!isLoaded || dropPath.length < 2) { - setRoutePath([]); - return; - } - let cancelled = false; - const ds = new window.google.maps.DirectionsService(); - const MAX_WPS = 23; // origin + 23 waypoints + destination = 25 stops/chunk - - const fetchSegment = (origin, destination, waypoints) => - new Promise((resolve, reject) => { - ds.route( - { - origin, - destination, - waypoints: waypoints.map((p) => ({ location: p, stopover: true })), - travelMode: window.google.maps.TravelMode.DRIVING - }, - (result, status) => { - if (status === 'OK') resolve(result); - else reject(new Error(status)); - } - ); - }); - - (async () => { - setRouteLoading(true); - try { - const points = dropPath; - const all = []; - let i = 0; - while (i < points.length - 1) { - const remaining = points.length - 1 - i; - const take = Math.min(remaining, MAX_WPS + 1); - const origin = points[i]; - const destination = points[i + take]; - const waypoints = points.slice(i + 1, i + take); - const res = await fetchSegment(origin, destination, waypoints); - const seg = res.routes[0].overview_path.map((ll) => ({ - lat: ll.lat(), - lng: ll.lng() - })); - // Avoid duplicating the join point between adjacent chunks. - if (all.length > 0 && seg.length > 0) seg.shift(); - all.push(...seg); - i += take; - } - if (!cancelled) setRoutePath(all); - } catch { - // Fall back to the straight-line skeleton on failure (quota, no route, etc.). - if (!cancelled) setRoutePath([]); - } finally { - if (!cancelled) setRouteLoading(false); - } - })(); - - return () => { - cancelled = true; - }; - }, [isLoaded, dropPath]); - - // Numbered step icon as a data URL — drawn fresh per render so we can pass - // the step number into the SVG without juggling external assets. Color is - // brand purple to match the planned-route polyline below. - const stepIcon = (n, isFocused) => { - const size = isFocused ? 38 : 32; - const color = isFocused ? '#900E1D' : '#C01227'; - const svg = encodeURIComponent( - `` + - `` + - `${n}` + - `` - ); - return `data:image/svg+xml;charset=UTF-8,${svg}`; - }; - - const headerBar = ( - - - - - Planned route{riderName ? ` — ${riderName}` : ''} - - {dateRange && ( - {dateRange} - )} - - {details && details.length > 0 && ( - - {details.length} {details.length === 1 ? 'stop' : 'stops'} - {routeLoading ? ' · resolving route…' : ''} - - )} - {onClose && ( - - - - )} - - ); - - // Loading state — route fetch in flight OR Google Maps script not ready yet. - if (loading || !isLoaded) { - return ( - - {headerBar} - - - - {loading ? 'Loading planned route…' : 'Loading map…'} - - - - ); - } - - // Empty state — fetched but rider has no deliveries with drop coords in the - // selected window. - if (!details || details.length === 0) { - return ( - - {headerBar} - - - No planned route for this rider - - - There are no deliveries with drop coordinates assigned to this rider for the selected date range. - - - - ); - } - - return ( - - {headerBar} - - (mapRef.current = map)} - center={dropPath[0]} - zoom={14} - options={{ - streetViewControl: false, - mapTypeControl: false, - fullscreenControl: false - }} - > - {routePath.length > 0 ? ( - <> - {/* Translucent backdrop so the route stays legible on busy tiles. */} - - {/* Road-following planned route from the Directions API. */} - - - ) : ( - // Fallback while Directions is in flight (or if it fails) — dashed - // straight-line skeleton between drop pins in step order. - - )} - - {details.map((d, i) => { - const stepNum = d.step || i + 1; - const isFocused = focusedStep === d.deliveryid; - return ( - setFocusedStep(isFocused ? null : d.deliveryid)} - zIndex={isFocused ? 1000 : stepNum} - > - {isFocused && ( - setFocusedStep(null)}> - - - Step {stepNum} · {d.customer} - - {d.address && ( - - {d.address} - - )} - {d.expectedTime && ( - - ETA {String(d.expectedTime).slice(11, 16) || d.expectedTime} - - )} - {d.orderid && ( - - Order #{d.orderid} - - )} - - - )} - - ); - })} - - - - ); -} diff --git a/src/pages/nearle/reports/mapWithRoute.js b/src/pages/nearle/reports/mapWithRoute.js deleted file mode 100644 index 251e5dd..0000000 --- a/src/pages/nearle/reports/mapWithRoute.js +++ /dev/null @@ -1,204 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { MapContainer, TileLayer, Marker, Polyline, Tooltip } from 'react-leaflet'; -import L from 'leaflet'; -import 'leaflet/dist/leaflet.css'; -import dayjs from 'dayjs'; -import { Chip, Stack, Typography, Box } from '@mui/material'; -import { CloseCircleOutlined } from '@ant-design/icons'; -import { useTheme } from '@mui/material/styles'; -import CircularLoader from 'components/CircularLoader'; - -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); - -// Start marker -const startIcon = new L.Icon({ - iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png', - shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', - iconSize: [25, 41], - iconAnchor: [12, 41], - shadowSize: [41, 41] -}); - -// End marker -const endIcon = new L.Icon({ - iconUrl: 'https://cdn.rawgit.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png', - shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', - iconSize: [25, 41], - iconAnchor: [12, 41], - shadowSize: [41, 41] -}); - -const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => { - console.log('additionalProps', additionalProps); - - const mapRef = useRef(null); - const theme = useTheme(); - const [routePoints, setRoutePoints] = useState([]); - const [loading, setLoading] = useState(false); - - // Fit the map to bounds - useEffect(() => { - if (mapRef.current && coordinates.length > 0) { - const bounds = [ - [Math.min(...coordinates.map((c) => c.lat)), Math.min(...coordinates.map((c) => c.lng))], - [Math.max(...coordinates.map((c) => c.lat)), Math.max(...coordinates.map((c) => c.lng))] - ]; - mapRef.current.fitBounds(bounds); - } - }, [coordinates]); - - // Fetch OSRM Route → REAL ROAD ROUTE - useEffect(() => { - const getOSRMRoute = async () => { - setLoading(true); - - // FIX: If only one coordinate, stop loader and exit - if (coordinates.length < 2) { - setRoutePoints([]); // no route - setLoading(false); - return; - } - - const subsample = (arr, max) => { - if (arr.length <= max) return arr; - const step = Math.ceil(arr.length / max); - const out = arr.filter((_, i) => i % step === 0); - const last = arr[arr.length - 1]; - if (out[out.length - 1] !== last) out.push(last); - return out; - }; - - // Attempt 1 — map-matching (best fidelity for dense traces). - try { - const ptsM = subsample(coordinates, 90); - const coordsM = ptsM.map((p) => `${p.lng},${p.lat}`).join(';'); - const matchUrl = `https://router.project-osrm.org/match/v1/driving/${coordsM}?overview=full&geometries=geojson&gaps=ignore&tidy=true`; - - const resM = await fetch(matchUrl); - const jsonM = await resM.json(); - if (jsonM.matchings && jsonM.matchings.length > 0) { - const poly = jsonM.matchings.flatMap((m) => - (m.geometry?.coordinates || []).map(([lng, lat]) => ({ lat, lng })) - ); - if (poly.length >= 2) { - setRoutePoints(poly); - setLoading(false); - return; - } - } - } catch (e) { - console.warn('OSRM Match error, trying route fallback:', e); - } - - // Attempt 2 — waypoint routing through a coarser subsample. - try { - const ptsR = subsample(coordinates, 25); - const coordsR = ptsR.map((p) => `${p.lng},${p.lat}`).join(';'); - const routeUrl = `https://router.project-osrm.org/route/v1/driving/${coordsR}?overview=full&geometries=geojson`; - - const resR = await fetch(routeUrl); - const jsonR = await resR.json(); - if (jsonR.routes && jsonR.routes[0]) { - const poly = jsonR.routes[0].geometry.coordinates.map(([lng, lat]) => ({ lat, lng })); - setRoutePoints(poly); - } else { - // Fallback to drawing direct lines between coordinates - setRoutePoints(coordinates); - } - } catch (err) { - console.error('OSRM Route fallback error:', err); - setRoutePoints(coordinates); - } finally { - setLoading(false); - } - }; - - getOSRMRoute(); - }, [coordinates]); - - if (!coordinates || coordinates.length === 0) return null; - - const start = coordinates[0]; - const end = coordinates[coordinates.length - 1]; - const center = coordinates[Math.floor(coordinates.length / 2)]; - - const InfoItem = ({ label, value }) => ( - - - {label}: - - - - ); - - return ( - - {loading && } - {/* CLOSE BUTTON */} - } - onClick={() => setMapOpen(false)} - sx={{ - position: 'absolute', - top: 12, - right: 12, - zIndex: 2000, - bgcolor: theme.palette.error.main, - color: '#fff', - fontWeight: 600, - borderRadius: '12px', - px: 1.5, - py: 0.5, - boxShadow: theme.shadows[4], - cursor: 'pointer', - '& .MuiChip-icon': { color: '#fff' } - }} - /> - - {/* MAP */} - - - - {/* START MARKER */} - - {`Pickup: ${dayjs(additionalProps.riderStart).format('DD-MM-YYYY hh:mm A')}`} - - - {/* END MARKER */} - - {`Drop: ${dayjs(additionalProps.riderEnd).format('DD-MM-YYYY hh:mm A')}`} - - - {/* REAL OSRM ROUTE */} - {routePoints.length > 0 && } - - - {/* BOTTOM DETAILS */} - - - - - - - - - - - - - ); -}; - -export default MapWithRoute; diff --git a/src/pages/nearle/reports/ordersDetails.js b/src/pages/nearle/reports/ordersDetails.js deleted file mode 100644 index c78bca8..0000000 --- a/src/pages/nearle/reports/ordersDetails.js +++ /dev/null @@ -1,1976 +0,0 @@ -import { React, useState, useEffect, useRef } from 'react'; -import axios from 'axios'; -import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; - -// material-ui -import { - Avatar, - Backdrop, - Box, - Button, - Chip, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - Divider, - Grid, - IconButton, - Paper, - Skeleton, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Tooltip, - Typography, - Autocomplete, - useMediaQuery -} from '@mui/material'; -import { useTheme } from '@mui/material/styles'; -import { - MdAssignment, - MdMyLocation, - MdGroups, - MdPlace, - MdDirectionsBike, - MdCalendarMonth, - MdFileDownload, - MdHourglassEmpty, - MdPersonPin, - MdLocationOn, - MdInventory2, - MdRoute, - MdSkipNext, - MdCheckCircle, - MdCancel, - MdList, - MdStraighten, - MdCurrencyRupee, - MdMap, - MdNoteAlt, - MdClose, - MdOutlineLocalShipping, - MdOutlineCheckCircle, - MdOutlinePendingActions, - MdOutlineCancel -} from 'react-icons/md'; -import { FaCircleCheck } from 'react-icons/fa6'; - -import MapWithRoute from './mapWithRoute'; -import CircularLoader from 'components/CircularLoader'; -import { fetchDeliveries, fetchRidersList, gettenantlocations, getTenants } from 'pages/api/api'; -import { CSVExport } from 'components/third-party/ReactTable'; -import Loader from 'components/Loader'; -import { enqueueSnackbar } from 'notistack'; -import DateFilterDialog from 'components/DateFilterDialog'; -import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; -import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; -import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; -import PageHeader from 'components/nearle_components/PageHeader'; -import StatCard from 'components/nearle_components/StatCard'; -import dayjs from 'dayjs'; -import { OpenToast } from 'components/third-party/OpenToast'; -import TableLoader from 'components/nearle_components/TableLoader'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); - -const opentoast = (message, variant, time) => { - enqueueSnackbar(message, { - variant: variant, - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: time ? time : 1500 - }); -}; - -// ============================================================================ -// Design tokens — shared with deliveries / tenants / customers / pricing pages. -// ============================================================================ -const DT = { - radiusPill: 999, - radiusCard: 14, - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', - shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)', - shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const a = (c, suffix) => `${c}${suffix}`; -const tint = (c) => a(c, '08'); -const soft = (c) => a(c, '18'); -const ring = (c) => a(c, '26'); -const edge = (c) => a(c, '55'); - -const BRAND = '#C01227'; -const BRAND_LIGHT = '#D35968'; - -const SoftPaper = (props) => ( - -); - -const AccentAvatar = ({ color, selected, size = 24, children }) => ( - - {children} - -); - -const pillFieldSx = (color) => ({ - '& .MuiOutlinedInput-root': { - borderRadius: '10px', - bgcolor: '#ffffff', - fontWeight: 600, - '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, - '&:hover fieldset': { borderColor: '#cbd5e1' }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` }, - '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 } - } -}); - -// Status visual meta — semantic colours, NOT brand. Each lifecycle state has -// its own colour so operators can recognise it at a glance. -const STATUS_META = { - all: { label: 'All', color: BRAND, icon: MdList }, - pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, - accepted: { label: 'Accepted', color: '#6366f1', icon: MdPersonPin }, - arrived: { label: 'Arrived', color: '#06b6d4', icon: MdLocationOn }, - picked: { label: 'Picked', color: '#8b5cf6', icon: MdInventory2 }, - active: { label: 'Active', color: '#14b8a6', icon: MdRoute }, - delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, - skipped: { label: 'Skipped', color: '#f97316', icon: MdSkipNext }, - cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel } -}; - -const STATUS_TABS = ['all', 'pending', 'accepted', 'arrived', 'picked', 'active', 'delivered', 'skipped', 'cancelled']; - -// Soft pill used for metric cells (km, charges) inside the table. -const MetricPill = ({ color, icon, label, tooltip }) => ( - - - {icon} - {label} - - -); - -// Stamp cell — date + time stack with skeleton fallback for empty timestamps. -const StampCell = ({ value, formatDate, formatTime, success }) => { - if (!value) { - return ( - - - - - ); - } - return ( - - - {formatDate(value)} - - - - {formatTime(value)} - - {success && } - - - ); -}; - -// ==============================|| Orders Details ||============================== // - -// Haversine distance between two [lat, lng] points in kilometers. -function haversineKm(a, b) { - const R = 6371; // km - const toRad = (d) => (d * Math.PI) / 180; - const lat1 = toRad(a[0]); - const lat2 = toRad(b[0]); - const dLat = toRad(b[0] - a[0]); - const dLon = toRad(b[1] - a[1]); - const s = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2; - return 2 * R * Math.asin(Math.min(1, Math.sqrt(s))); -} - -function kalmanSmoothGps(pings, options = {}) { - if (!Array.isArray(pings) || pings.length === 0) return []; - - // 1. Filter out obviously invalid coordinate pings (e.g. 0,0 or NaN) - const cleanedPings = pings.filter(p => - Number.isFinite(p.lat) && - Number.isFinite(p.lng) && - (Math.abs(p.lat) > 0.1 || Math.abs(p.lng) > 0.1) - ); - - if (cleanedPings.length === 0) return []; - if (cleanedPings.length === 1) { - return [{ lat: cleanedPings[0].lat, lng: cleanedPings[0].lng, logdate: cleanedPings[0].logdate, _ts: cleanedPings[0]._ts }]; - } - - const processNoise = - options.processNoise != null ? options.processNoise : 1e-10; - const measurementNoise = - options.measurementNoise != null ? options.measurementNoise : 2e-9; - const outlierGate = - options.outlierGate != null ? options.outlierGate : 9.0; - const maxSpeedKmh = - options.maxSpeedKmh != null ? options.maxSpeedKmh : 120; - - const tsOf = (p) => - p._ts || (p.logdate ? new Date(p.logdate).getTime() : 0); - - // 2. Scan forward to find the first valid starting anchor - let startIdx = 0; - while (startIdx < cleanedPings.length - 1) { - const p0 = cleanedPings[startIdx]; - const p1 = cleanedPings[startIdx + 1]; - const ts0 = tsOf(p0); - const ts1 = tsOf(p1) || ts0 + 1000; - const dtSec = Math.max(0.001, (ts1 - ts0) / 1000); - const km = haversineKm([p0.lat, p0.lng], [p1.lat, p1.lng]); - const speedKmh = (km / dtSec) * 3600; - - if (speedKmh <= maxSpeedKmh) { - break; - } else { - // Speed is too high. Check if p1->p2 is normal (meaning p0 is the outlier) - if (startIdx + 2 < cleanedPings.length) { - const p2 = cleanedPings[startIdx + 2]; - const ts2 = tsOf(p2) || ts1 + 1000; - const dtSec12 = Math.max(0.001, (ts2 - ts1) / 1000); - const km12 = haversineKm([p1.lat, p1.lng], [p2.lat, p2.lng]); - const speedKmh12 = (km12 / dtSec12) * 3600; - - if (speedKmh12 <= maxSpeedKmh) { - startIdx = startIdx + 1; - continue; - } - } - startIdx++; - } - } - - // 3. Teleport filter starting from the valid anchor - const accepted = [cleanedPings[startIdx]]; - let lastTs = tsOf(cleanedPings[startIdx]); - for (let i = startIdx + 1; i < cleanedPings.length; i++) { - const p = cleanedPings[i]; - const ts = tsOf(p) || lastTs + 1000; - const dtSec = Math.max(0.001, (ts - lastTs) / 1000); - const prev = accepted[accepted.length - 1]; - const km = haversineKm([prev.lat, prev.lng], [p.lat, p.lng]); - const speedKmh = (km / dtSec) * 3600; - if (speedKmh > maxSpeedKmh) continue; - accepted.push(p); - lastTs = ts; - } - - if (accepted.length < 2) { - return accepted.map((p) => ({ lat: p.lat, lng: p.lng, logdate: p.logdate, _ts: p._ts })); - } - - // Run a 1D Kalman + RTS smoother over one axis. Returns smoothed - // positions parallel to `accepted`. - const smoothAxis = (axisKey) => { - const N = accepted.length; - const xPost = new Array(N); - const pPost = new Array(N); - const xPrior = new Array(N); - const pPrior = new Array(N); - const dtArr = new Array(N); - - const ts0 = tsOf(accepted[0]); - const ts1 = tsOf(accepted[1]); - const dt01 = Math.max(0.1, (ts1 - ts0) / 1000); - const v0 = (accepted[1][axisKey] - accepted[0][axisKey]) / dt01; - xPost[0] = [accepted[0][axisKey], v0]; - pPost[0] = [measurementNoise, 0, 0, 1]; - xPrior[0] = xPost[0].slice(); - pPrior[0] = pPost[0].slice(); - dtArr[0] = 0; - - let prevTs = ts0; - for (let i = 1; i < N; i++) { - const ts = tsOf(accepted[i]) || prevTs + 1000; - const dt = Math.max(0.1, (ts - prevTs) / 1000); - prevTs = ts; - dtArr[i] = dt; - - // Predict - const [xPrev, vPrev] = xPost[i - 1]; - const xPredPos = xPrev + vPrev * dt; - const xPredVel = vPrev; - const [pp00, pp01, pp10, pp11] = pPost[i - 1]; - const dt2 = dt * dt; - const dt3 = dt2 * dt; - const dt4 = dt3 * dt; - const np00 = pp00 + dt * (pp01 + pp10) + dt2 * pp11 + (dt4 / 4) * processNoise; - const np01 = pp01 + dt * pp11 + (dt3 / 2) * processNoise; - const np10 = pp10 + dt * pp11 + (dt3 / 2) * processNoise; - const np11 = pp11 + dt2 * processNoise; - xPrior[i] = [xPredPos, xPredVel]; - pPrior[i] = [np00, np01, np10, np11]; - - // Update - const z = accepted[i][axisKey]; - const y = z - xPredPos; - const S = np00 + measurementNoise; - const mahal2 = (y * y) / S; - if (mahal2 > outlierGate) { - xPost[i] = [xPredPos, xPredVel]; - pPost[i] = [np00, np01, np10, np11]; - continue; - } - const K0 = np00 / S; - const K1 = np10 / S; - const newPos = xPredPos + K0 * y; - const newVel = xPredVel + K1 * y; - xPost[i] = [newPos, newVel]; - pPost[i] = [ - (1 - K0) * np00, - (1 - K0) * np01, - np10 - K1 * np00, - np11 - K1 * np01 - ]; - } - - // RTS backward smoother - const xSmooth = new Array(N); - xSmooth[N - 1] = xPost[N - 1].slice(); - for (let i = N - 2; i >= 0; i--) { - const dt = dtArr[i + 1]; - const [pp00, pp01, pp10, pp11] = pPost[i]; - const a = pp00 + dt * pp01; - const b = pp01; - const c = pp10 + dt * pp11; - const d = pp11; - const [q00, q01, q10, q11] = pPrior[i + 1]; - const det = q00 * q11 - q01 * q10; - if (!Number.isFinite(det) || Math.abs(det) < 1e-30) { - xSmooth[i] = xPost[i].slice(); - continue; - } - const inv00 = q11 / det; - const inv01 = -q01 / det; - const inv10 = -q10 / det; - const inv11 = q00 / det; - const c00 = a * inv00 + b * inv10; - const c01 = a * inv01 + b * inv11; - const c10 = c * inv00 + d * inv10; - const c11 = c * inv01 + d * inv11; - const dxPos = xSmooth[i + 1][0] - xPrior[i + 1][0]; - const dxVel = xSmooth[i + 1][1] - xPrior[i + 1][1]; - xSmooth[i] = [ - xPost[i][0] + c00 * dxPos + c01 * dxVel, - xPost[i][1] + c10 * dxPos + c11 * dxVel - ]; - } - - return xSmooth.map((s) => s[0]); - }; - - const lats = smoothAxis('lat'); - const lngs = smoothAxis('lng'); - return accepted.map((p, i) => ({ - lat: lats[i], - lng: lngs[i], - logdate: p.logdate, - _ts: p._ts - })); -} - -export default function OrdersDetails() { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const loadMoreRef = useRef(); - const containerRef = useRef(); - const locationRef = useRef(null); - const tenantRef = useRef(null); - const userid = localStorage.getItem('userid'); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(50); - const [locaName, setLocoName] = useState('All'); - const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); - const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); - const [open, setOpen] = useState(false); - const [mapOpen, setMapOpen] = useState(false); - const [datestatus, setDatestatus] = useState('Today'); - const [appId, setAppId] = useState(0); - const [searchword, setSearchword] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - const [riderCoordinates, setRiderCoordinates] = useState([]); - const [riderStart, setRiderStart] = useState(); - const [riderEnd, setRiderEnd] = useState(); - const [mapTenant, setMapTenant] = useState({}); - const [isLoading, setIsLoading] = useState(false); - let [total, settotal] = useState(0); - let [deliveredLenght, setDeliveredLenght] = useState(0); - let [pendingLenght, setPendingLenght] = useState(0); - let [cancelLenght, setCancelLenght] = useState(0); - let [assignLenght, setAssignLenght] = useState(0); - let [pickedLenght, setPickedLenght] = useState(0); - let [activeLenght, setActiveLenght] = useState(0); - let [arrivesLenght, setArrivedLenght] = useState(0); - let [skippedLenght, setSkippedLenght] = useState(0); - const [currentStatus, setCurrentStatus] = useState('All'); - const [locationid, setLocationid] = useState(0); - const [tenantid, setTenantid] = useState(0); - const [tenantValue, setTenantValue] = useState(null); - const [locationValue, setLocationValue] = useState(null); - const [selectedRider, setSelectedRider] = useState(); - const [riderValue, setRiderValue] = useState(null); - const [reportDialog, setReportDialog] = useState(false); - const [logsLoading, setLogsLoading] = useState(false); - - // Map status key (lowercase) → count value from the summary endpoint. - const statusCountByKey = { - all: total, - pending: pendingLenght, - accepted: assignLenght, - arrived: arrivesLenght, - picked: pickedLenght, - active: activeLenght, - delivered: deliveredLenght, - skipped: skippedLenght, - cancelled: cancelLenght - }; - - // Cascading clears so changing a parent filter resets its children. - useEffect(() => { - setTenantid(0); - setTenantValue(null); - setLocationid(0); - setLocationValue(null); - setSelectedRider(null); - setRiderValue(null); - }, [appId]); - - useEffect(() => { - setLocationid(0); - setLocationValue(null); - setRiderValue(null); - }, [tenantid]); - - useEffect(() => { - setRiderValue(null); - }, [locationid]); - - // ============== Haversine distance calculation for the map route ============== - function calculateDistance(lat1, lon1, lat2, lon2) { - const R = 6371; - const dLat = (lat2 - lat1) * (Math.PI / 180); - const dLon = (lon2 - lon1) * (Math.PI / 180); - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; - } - - function calculateTotalDistance(routeCoordinates) { - let totalDistance = 0; - for (let i = 0; i < routeCoordinates.length - 1; i++) { - const { lat: lat1, lng: lon1 } = routeCoordinates[i]; - const { lat: lat2, lng: lon2 } = routeCoordinates[i + 1]; - totalDistance += calculateDistance(lat1, lon1, lat2, lon2); - } - return totalDistance; - } - - const getdeliverylogs = async (id) => { - setLogsLoading(true); - try { - const res = await axios.get(`${process.env.REACT_APP_URL3}/deliveries/getdeliverylogs/?deliveryid=${id}`); - const datas = res.data.details; - if (Array.isArray(datas) && datas.length !== 0) { - // Sort chronologically by logdate - const sorted = datas - .map((r) => { - const ts = r?.logdate ? dayjs(r.logdate) : null; - return { - lat: parseFloat(r?.latitude ?? r?.lat), - lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon), - logdate: r?.logdate, - _ts: ts && ts.isValid() ? ts.valueOf() : Number.MAX_SAFE_INTEGER - }; - }) - .filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng)) - .sort((a, b) => a._ts - b._ts); - - if (sorted.length !== 0) { - setRiderStart(sorted[0].logdate); - setRiderEnd(sorted[sorted.length - 1].logdate); - - // Apply Kalman filter - const smoothed = kalmanSmoothGps(sorted); - const coData = smoothed.map((data) => ({ lat: data.lat, lng: data.lng })); - setRiderCoordinates(coData); - calculateTotalDistance(coData); - setMapOpen(true); - } else { - opentoast('No Valid Logs Found', 'error', 2000); - } - } else { - opentoast('No Logs Found ', 'error', 2000); - } - } catch (error) { - console.log('getdeliverylogs', error); - } finally { - setLogsLoading(false); - } - }; - - // ==============================|| fetchDeliveries (infinite) ||============================== // - - const { - data: deliveriesData, - isLoading: fetchDeliveriesIsLoading, - isError: fetchDeliveriesIsError, - error: fetchDeliveriesError, - fetchNextPage, - hasNextPage, - isFetchingNextPage - } = useInfiniteQuery({ - queryKey: [ - 'fetchdeliveries', - appId, - userid, - currentStatus, - startdate, - enddate, - rowsPerPage, - debouncedSearch, - tenantid, - locationid, - selectedRider?.userid || 0 - ], - queryFn: fetchDeliveries, - getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined, - refetchOnWindowFocus: true, - refetchOnMount: true, - refetchOnReconnect: true - }); - const rows = deliveriesData?.pages.flatMap((page) => page.rows) || []; - - useEffect(() => { - if (!hasNextPage) return; - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) { - fetchNextPage(); - } - }, - { - root: document.querySelector('.MuiTableContainer-root'), - rootMargin: '0px', - threshold: 1.0 - } - ); - if (loadMoreRef.current) observer.observe(loadMoreRef.current); - return () => { - if (loadMoreRef.current) observer.unobserve(loadMoreRef.current); - }; - }, [hasNextPage, fetchNextPage]); - - const handleScroll = (event) => { - const { scrollTop, scrollHeight, clientHeight } = event.currentTarget; - if (scrollTop + clientHeight >= scrollHeight - 50) { - if (hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - } - }; - - // ==============================|| Tenant / Location / Rider lookups ||============================== // - - const { - data: tenantlist, - isLoading: fetchtenantsIsLoading, - isError: fetchtenantsIsError, - error: fetchtenantsError - } = useQuery({ - queryKey: ['tenantlist', appId], - queryFn: () => getTenants(appId), - enabled: appId !== 0 - }); - - const { - data: ridersList, - isLoading: getriderbydeliveryIsLoading, - isError: getriderbydeliveryIsError, - error: getriderbydeliveryError - } = useQuery({ - queryKey: ['fetchRidersList', appId], - queryFn: fetchRidersList, - enabled: appId !== 0 - }); - - const { - data: locationlist, - isLoading: fetchlocationsIsLoading, - isError: fetchlocationsIsError, - error: fetchlocationsError - } = useQuery({ - queryKey: ['gettenantlocations', tenantid], - queryFn: () => gettenantlocations(tenantid), - enabled: tenantid !== 0 - }); - - // ==============================|| status summary counts ||============================== // - const fetchcount = async () => { - setIsLoading(true); - try { - await axios - .get( - appId == 0 - ? `${process.env.REACT_APP_URL}/deliveries/deliverysummary/?fromdate=${startdate}&todate=${enddate}` - : `${ - process.env.REACT_APP_URL - }/deliveries/deliverysummary/?applocationid=${appId}&tenantid=${tenantid}&locationid=${locationid}&fromdate=${startdate}&todate=${enddate}&userid=${ - selectedRider?.userid || 0 - }` - ) - .then((res) => { - settotal(res.data.details.total); - setPendingLenght(res.data.details.pending); - setAssignLenght(res.data.details.accepted); - setArrivedLenght(res.data.details.arrived); - setPickedLenght(res.data.details.picked); - setActiveLenght(res.data.details.active); - setDeliveredLenght(res.data.details.delivered); - setSkippedLenght(res.data.details.skipped); - setCancelLenght(res.data.details.cancelled); - }) - .catch((err) => { - enqueueSnackbar(err.message, { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - }); - } catch (err) { - console.log(err); - } finally { - setIsLoading(false); - } - }; - useEffect(() => { - fetchcount(); - }, [appId, startdate, enddate, currentStatus, tenantid, locationid, selectedRider]); - - // CSV export payload — flat schema preserved for backwards compatibility. - const csvData = rows?.map((order) => ({ - tenantname: order.tenantname, - tenantcity: order.tenantcity, - tenantcontactno: order.tenantcontactno, - rider: order.ridername, - orderid: order.orderid, - paymenttype: order.paymenttype == 64 ? 'Pay Later' : order.paymenttype == 42 ? 'Pay on Delivery' : 'Digital', - deliverydate: order.deliverydate, - orderstatus: order.orderstatus, - ordernotes: order.ordernotes, - kms: order.kms, - cumulativekms: order.cumulativekms, - assigntime: order.assigntime, - starttime: order.starttime, - arrivaltime: order.arrivaltime, - pickuptime: order.pickuptime, - deliverytime: order.deliverytime, - canceltime: order.canceltime, - deliverycharge: order.deliverycharges, - deliveryamt: order.deliveryamt, - pickupcustomer: order.pickupcustomer, - pickupcontactno: order.pickupcontactno, - Pickupaddress: order.pickupaddress, - pickupsuburb: order.pickupsuburb, - pickupcity: order.applocation, - pickuplat: order.pickuplat, - pickuplong: order.pickuplon, - deliverycustomer: order.deliverycustomer, - deliverycontactno: order.deliverycontactno, - deliveryaddress: order.deliveryaddress, - deliverysuburb: order.locationsuburb, - deliverylat: order.deliverylat, - deliverylong: order.deliverylong, - locationname: order.locationname, - locationsuburb: order.pickuplocation, - deliverylocation: order.deliverylocation, - locationcontactno: order.locationcontactno - })); - - function formatDate(dateString) { - return dayjs(dateString).format('DD/MM/YYYY '); - } - function formatTime(dateString) { - return dayjs(dateString).format(' hh:mm A'); - } - - const errormessage = fetchDeliveriesIsError - ? `An error has occurred: (fetchDeliveries) ${fetchDeliveriesError.message}` - : fetchtenantsIsError - ? `An error has occurred: (getTenants) ${fetchtenantsError.message}` - : fetchlocationsIsError - ? `An error has occurred: (gettenantlocations) ${fetchlocationsError.message}` - : getriderbydeliveryIsError - ? `An error has occurred: (getriderbydelivery) ${getriderbydeliveryError.message}` - : null; - - useEffect(() => { - if (errormessage) { - opentoast(errormessage, 'warning', 2000); - } - }, [errormessage]); - - const KPI_META = [ - { key: 'total', label: 'Total Orders', color: BRAND, icon: MdOutlineLocalShipping, value: total }, - { key: 'delivered', label: 'Delivered', color: '#10b981', icon: MdOutlineCheckCircle, value: deliveredLenght }, - { key: 'pending', label: 'Pending', color: '#f59e0b', icon: MdOutlinePendingActions, value: pendingLenght }, - { key: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdOutlineCancel, value: cancelLenght } - ]; - - return ( - <> - {(isLoading || - fetchtenantsIsLoading || - fetchlocationsIsLoading || - logsLoading || - getriderbydeliveryIsLoading || - isFetchingNextPage) && ( -
- -
- )} - {fetchDeliveriesIsLoading && ( - theme.zIndex.drawer + 1 - }} - open={fetchDeliveriesIsLoading} - /> - )} - - {/* ============================================= || Header || ============================================= */} - } - placeholder="Select Zone" - paperComponent={SoftPaper} - sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }} - /> - } - /> - - {/* ============================================= || KPI Cards || ============================================= */} - - {KPI_META.map((item) => { - const Icon = item.icon; - return ( - - } - color={item.color} - loading={isLoading} - /> - - ); - })} - - - {/* ============================================= || Filter Bar (tenant, location, rider, date, export) || ============================================= */} - - - - option?.tenantname || ''} - PaperComponent={SoftPaper} - onOpen={(event) => { - if (!appId) { - event.preventDefault(); - OpenToast('Please select a your app location first!', 'warning', 3000); - setTimeout(() => { - locationRef.current?.focus(); - }, 0); - } - }} - onChange={(e, val, reason) => { - if (reason === 'clear') { - setTenantid(0); - setTenantValue(null); - setLocationid(0); - setLocationValue(null); - } else { - setTenantid(val?.tenantid || 0); - setTenantValue(val); - setLocationid(val.locationid); - setLocationValue(null); - } - }} - renderInput={(params) => ( - - - - - - ) - }} - /> - )} - /> - - - - (option ? `${option.locationname} (${option.suburb})` : '')} - value={locationValue} - PaperComponent={SoftPaper} - onOpen={(event) => { - if (!appId && !tenantid) { - event.preventDefault(); - OpenToast('Please select a your Location and Tenant first!', 'warning', 3000); - setTimeout(() => { - locationRef.current?.focus(); - }, 0); - } else if (!tenantid) { - event.preventDefault(); - OpenToast('Please select a your Tenant first!', 'warning', 3000); - setTimeout(() => { - tenantRef.current?.focus(); - }, 0); - } - }} - onChange={(e, val, reason) => { - if (reason === 'clear') { - setLocationid(0); - setLocationValue(null); - } else { - setLocationid(val.locationid || 0); - setLocationValue(val); - } - }} - renderInput={(params) => ( - - - - - - ) - }} - /> - )} - /> - - - - `${option.firstname} ${option.lastname}`} - PaperComponent={SoftPaper} - onOpen={() => { - if (!appId) { - OpenToast('Select App Location First', 'warning', 2000); - } - }} - onChange={(event, value, reason) => { - if (reason === 'clear') { - setSelectedRider(null); - setRiderValue(null); - } else { - setSelectedRider(value); - setRiderValue(value); - } - }} - renderInput={(params) => ( - - - - - - ) - }} - /> - )} - /> - - - - - - setOpen(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')} - - - - - - - - - {/* ============================================= || Status Tabs + Search || ============================================= */} - - - - {STATUS_TABS.map((key) => { - const meta = STATUS_META[key]; - const Icon = meta.icon; - const active = currentStatus.toLowerCase() === key; - const count = statusCountByKey[key] ?? 0; - return ( - setCurrentStatus(key === 'all' ? 'All' : key)} - 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: `1px solid ${active ? meta.color : DT.borderSubtle}`, - bgcolor: active ? meta.color : DT.surface, - color: active ? '#fff' : DT.textSecondary, - fontWeight: 600, - boxShadow: 'none', - transition: 'background-color 0.15s, border-color 0.15s, color 0.15s', - '&:hover': { - borderColor: active ? meta.color : '#cbd5e1', - bgcolor: active ? meta.color : DT.surfaceAlt - } - }} - > - - - - - {meta.label} - - - {count} - - - ); - })} - - - - - - - - - {/* ============================================= || Table || ============================================= */} - - - {isMobile ? ( - /* ===================== MOBILE: card list ===================== */ - - {fetchDeliveriesIsLoading ? ( - - - - Loading orders… - - - ) : rows?.length == 0 ? ( - - - - - - No orders to show - - - {searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'} - - - ) : ( - rows?.map((row, index) => { - const statusKey = String(row.orderstatus || '').toLowerCase(); - const rowStatusMeta = STATUS_META[statusKey] || { - label: row.orderstatus || '—', - color: BRAND, - icon: MdAssignment - }; - const StatusIcon = rowStatusMeta.icon; - const cancelled = statusKey === 'cancelled'; - const isDelivered = row.orderstatus === 'delivered'; - return ( - - - - - {String(page * rowsPerPage + index + 1).padStart(2, '0')} - - - - - - - {row.tenantname} - - - #{row.orderid} - - - - - - { - if (isDelivered) { - getdeliverylogs(row.deliveryid); - setMapTenant(row); - } - }} - sx={{ - flexShrink: 0, - bgcolor: isDelivered ? soft(BRAND) : soft('#94a3b8'), - color: isDelivered ? BRAND : DT.textMuted, - border: `1px solid ${isDelivered ? edge(BRAND) : edge('#94a3b8')}`, - '&:hover': { - bgcolor: isDelivered ? BRAND : soft('#94a3b8'), - color: isDelivered ? '#fff' : DT.textMuted - } - }} - > - - - - - - - - {rowStatusMeta.label} - - {row.ridername && ( - - {row.ridername} - - )} - - {dayjs(row.deliverydate).utc().format('DD/MM/YYYY · hh:mm A')} - - - - } - > - - - - - {row.pickupcustomer || '—'} - - - {row.pickupcontactno} - - - {row.pickupsuburb || (row.Pickupaddress ? row.Pickupaddress.slice(0, 22) + '…' : '')} - - {row.applocation && ( - - {row.applocation} - - )} - - - - - - {row.deliverycustomer || '—'} - - - {row.deliverycontactno} - - - {row.deliverysuburb || (row.deliveryaddress ? row.deliveryaddress.slice(0, 22) + '…' : '')} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - } - label={cancelled || row.kms == '' ? '0 km' : `${row.kms} km`} - tooltip="KMS" - /> - } - label={`${row.cumulativekms ?? 0} km`} - tooltip="Actual KMS" - /> - } - label={`${row.previouskms || (cancelled ? '0.00' : row.kms) || 0} km`} - tooltip="Rider KMS" - /> - - - - - } - label={cancelled || row.deliverycharges == '' ? `0.00` : `${row.deliverycharges}.00`} - tooltip="Delivery Charge" - /> - } - label={row.deliveryamt == '' ? `0.00` : `${row.deliveryamt}.00`} - tooltip="Delivery Amount" - /> - - - - {row.ordernotes && ( - - - - - {row.ordernotes} - - - - )} - - - ); - }) - )} - - ) : ( - - - - # - Map - Client - Pickup - Drop - Status / Rider - Assigned - Accepted - Arrived - Picked - Active - Delivered - Cancelled - Notes - KMS - Charges - - - - - {fetchDeliveriesIsLoading ? ( - - ) : rows?.length == 0 ? ( - - - - - - - - No orders to show - - - {searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'} - - - - - ) : ( - rows?.map((row, index) => { - const statusKey = String(row.orderstatus || '').toLowerCase(); - const rowStatusMeta = STATUS_META[statusKey] || { - label: row.orderstatus || '—', - color: BRAND, - icon: MdAssignment - }; - const StatusIcon = rowStatusMeta.icon; - const cancelled = statusKey === 'cancelled'; - return ( - - - - {String(page * rowsPerPage + index + 1).padStart(2, '0')} - - - - {/* ====================== Map button ====================== */} - - - - { - if (row.orderstatus === 'delivered') { - getdeliverylogs(row.deliveryid); - setMapTenant(row); - } - }} - sx={{ - bgcolor: row.orderstatus === 'delivered' ? soft(BRAND) : soft('#94a3b8'), - color: row.orderstatus === 'delivered' ? BRAND : DT.textMuted, - border: `1px solid ${row.orderstatus === 'delivered' ? edge(BRAND) : edge('#94a3b8')}`, - '&:hover': { - bgcolor: row.orderstatus === 'delivered' ? BRAND : soft('#94a3b8'), - color: row.orderstatus === 'delivered' ? '#fff' : DT.textMuted - } - }} - > - - - - - - - {/* ====================== Client ====================== */} - - - - - - - - {row.tenantname} - - - - #{row.orderid} - - - - {dayjs(row.deliverydate).utc().format('DD/MM/YYYY · hh:mm A')} - - - - - - {/* ====================== Pickup ====================== */} - - - - {row.pickupcustomer || '—'} - - - {row.pickupcontactno} - - - - {row.pickupsuburb || (row.Pickupaddress ? row.Pickupaddress.slice(0, 22) + '…' : '')} - - - {row.applocation && ( - - {row.applocation} - - )} - - - - {/* ====================== Drop ====================== */} - - - - {row.deliverycustomer || '—'} - - - {row.deliverycontactno} - - - - {row.deliverysuburb || (row.deliveryaddress ? row.deliveryaddress.slice(0, 22) + '…' : '')} - - - - - - {/* ====================== Status / Rider ====================== */} - - - - {rowStatusMeta.label} - - {row.ridername && ( - - - - - {row.ridername} - - - - )} - - - - {/* ====================== Timestamps ====================== */} - - - - - - - - - - - - - - - - - - - - - - - {/* ====================== Notes ====================== */} - - {row.ordernotes ? ( - - - - {row.ordernotes} - - - ) : ( - - )} - - - {/* ====================== KMS ====================== */} - - - } - label={cancelled || row.kms == '' ? '0 km' : `${row.kms} km`} - tooltip="KMS" - /> - } - label={`${row.cumulativekms ?? 0} km`} - tooltip="Actual KMS" - /> - } - label={`${row.previouskms || (cancelled ? '0.00' : row.kms) || 0} km`} - tooltip="Rider KMS" - /> - - - - {/* ====================== Charges ====================== */} - - - } - label={cancelled || row.deliverycharges == '' ? `0.00` : `${row.deliverycharges}.00`} - tooltip="Delivery Charge" - /> - } - label={row.deliveryamt == '' ? `0.00` : `${row.deliveryamt}.00`} - tooltip="Delivery Amount" - /> - - - - ); - }) - )} - -
- )} - - {rows?.length !== 0 && ( - - - {isFetchingNextPage || hasNextPage ? ( - - ) : ( - - No more orders - - )} - - - )} -
-
- - {/* ============================================= || Export Dialog || ============================================= */} - setReportDialog(false)} - fullWidth - maxWidth="sm" - fullScreen={isMobile} - PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 }, overflow: 'hidden' } }} - > - - - - - - - - - Report - - - Export Orders - - - - setReportDialog(false)} - sx={{ color: '#fff', bgcolor: 'rgba(255,255,255,0.18)', '&:hover': { bgcolor: 'rgba(255,255,255,0.3)' } }} - > - - - - - - {fetchDeliveriesIsLoading && } - {[ - { label: 'App Location', value: locaName, color: BRAND }, - { label: 'Tenant', value: tenantValue?.tenantname, color: '#0ea5e9' }, - { label: 'Business Location', value: locationValue?.locationname, color: '#10b981' }, - { label: 'Status', value: currentStatus, color: '#f59e0b' }, - { label: 'Rider', value: riderValue ? `${riderValue.firstname} ${riderValue.lastname}` : null, color: '#8b5cf6' }, - { label: 'Keyword', value: searchword, color: '#06b6d4' }, - { label: 'Start Date', value: startdate, color: '#14b8a6' }, - { label: 'End Date', value: enddate, color: '#ef4444' } - ].map((item, idx) => ( - - - {item.label} - - - - ))} - - - - { - setTimeout(() => setReportDialog(false), 0); - }} - /> - - - - {/* ============================================= || Date Filter Dialog || ============================================= */} - setOpen(false)} - onSelect={(range) => { - setStartdate(range.startDate); - setEnddate(range.endDate); - setDatestatus(range.label); - }} - /> - - {/* ============================================= || Map Dialog || ============================================= */} - { - setMapOpen(false); - }} - fullScreen - fullWidth - > - {riderCoordinates && ( -
- -
- )} -
- - ); -} diff --git a/src/pages/nearle/reports/ordersSummary.js b/src/pages/nearle/reports/ordersSummary.js deleted file mode 100644 index 2906ab0..0000000 --- a/src/pages/nearle/reports/ordersSummary.js +++ /dev/null @@ -1,1372 +0,0 @@ -import React, { useState, useEffect, useMemo, useRef } from 'react'; -import axios from 'axios'; -import { useQuery } from '@tanstack/react-query'; - -// material-ui -import { - Autocomplete, - Avatar, - Box, - Chip, - Collapse, - Divider, - Grid, - IconButton, - Paper, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Tooltip, - Typography, - useMediaQuery, - useTheme -} from '@mui/material'; -import { - MdAssignment, - MdCalendarMonth, - MdCancel, - MdCheckCircle, - MdCurrencyRupee, - MdExpandLess, - MdExpandMore, - MdGroups, - MdHourglassEmpty, - MdLocalShipping, - MdLocationOn, - MdMyLocation, - MdPerson, - MdStraighten, - MdStore, - MdOutlineLocalShipping, - MdOutlinePendingActions, - MdOutlineCheckCircle, - MdOutlineCurrencyRupee -} from 'react-icons/md'; - -import dayjs from 'dayjs'; -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); - -import { getreportlocationsummary, getreportsummary, gettenantlocations, getTenants } from 'pages/api/api'; -import Loader from 'components/Loader'; -import DateFilterDialog from 'components/DateFilterDialog'; -import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; -import PageHeader from 'components/nearle_components/PageHeader'; -import StatCard from 'components/nearle_components/StatCard'; -import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; -import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; -import { OpenToast } from 'components/third-party/OpenToast'; - -// ============================================================================ -// Design tokens — shared with deliveries / ridersSummary / customers / -// tenants pages. -// ============================================================================ -const DT = { - radiusPill: 999, - radiusCard: 14, - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', - shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)', - shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const a = (c, suffix) => `${c}${suffix}`; -const tint = (c) => a(c, '08'); -const soft = (c) => a(c, '18'); -const ring = (c) => a(c, '26'); -const edge = (c) => a(c, '55'); - -const BRAND = '#C01227'; -const BRAND_LIGHT = '#D35968'; - -const SoftPaper = (props) => ( - -); - -const AccentAvatar = ({ color, selected, size = 24, children }) => ( - - {children} - -); - -// Pill used in cells for km / amount metrics. -const MetricPill = ({ color, icon, label, tooltip, minWidth = 80 }) => ( - - - {icon} - {label} - - -); - -// Coloured numeric cell — non-zero values get a soft accent badge to draw -// the eye, preserving the legacy "red when present" affordance with the -// modern soft-pill aesthetic. -const CountCell = ({ value, color = '#ef4444', icon }) => { - const n = Number(value) || 0; - if (n === 0) { - return ( - - 0 - - ); - } - return ( - - {icon} - {n} - - ); -}; - -// Pill-style filter inputs for the Tenant / Location autocompletes. -const pillFieldSx = (color) => ({ - '& .MuiOutlinedInput-root': { - borderRadius: '10px', - bgcolor: '#ffffff', - fontWeight: 600, - '& fieldset': { borderColor: '#e2e8f0', borderWidth: 1 }, - '&:hover fieldset': { borderColor: '#cbd5e1' }, - '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` }, - '&.Mui-focused fieldset': { borderColor: '#C01227', borderWidth: 1.5 } - } -}); - -function formatNumberToRupees(value) { - return new Intl.NumberFormat('en-IN', { - style: 'currency', - currency: 'INR', - minimumFractionDigits: 2 - }).format(Number(value) || 0); -} - -// ============================================================================ -// Orders Summary -// ============================================================================ -export default function OrdersReport() { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - - const locationRef = useRef(null); - const tenantRef = useRef(null); - - const [appId, setAppId] = useState(0); - const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); - const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); - const [locaName, setLocoName] = useState('All'); - const [open, setOpen] = useState(false); - const [openRow, setOpenRow] = useState(null); - const [datestatus, setDatestatus] = useState('Today'); - const [ridersdata, setRidersdata] = useState([]); - const [loading, setLoading] = useState(false); - const [locationid, setLocationid] = useState(0); - const [tenantid, setTenantid] = useState(0); - const [tenantValue, setTenantValue] = useState(null); - const [locationValue, setLocationValue] = useState(null); - const [searchword, setSearchword] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - const [, setPage] = useState(0); - - // Clear nested filters when scope widens. - useEffect(() => { - setTenantid(0); - setTenantValue(null); - setLocationid(0); - setLocationValue(null); - setOpenRow(null); - }, [appId]); - - useEffect(() => { - setLocationid(0); - setLocationValue(null); - setOpenRow(null); - }, [tenantid]); - - // ==============================|| primary summary query ||============================== // - // The api.js destructure expects [appId, tenantid, locationid, startdate, enddate] - // — do NOT prepend a name slot without updating api.js in lockstep. - const { - data: rows, - isLoading: isLoadingReports, - isError: isErrorReports, - error: reportsError - } = useQuery({ - queryKey: [appId, tenantid, locationid, startdate, enddate], - queryFn: tenantid ? getreportlocationsummary : getreportsummary - }); - - // ==============================|| tenant list ||============================== // - const { data: tenantlist } = useQuery({ - queryKey: ['tenantlist', appId], - queryFn: () => getTenants(appId), - enabled: appId !== 0 - }); - - // ==============================|| tenant locations ||============================== // - const { data: locationlist } = useQuery({ - queryKey: ['gettenantlocations', tenantid], - queryFn: () => gettenantlocations(tenantid), - enabled: tenantid !== 0 - }); - - // Defensive: backend sometimes returns null for empty results. - const safeRows = useMemo(() => (Array.isArray(rows) ? rows : []), [rows]); - - // Client-side filter on tenant / location name. - const filteredRows = useMemo(() => { - if (!debouncedSearch) return safeRows; - const q = debouncedSearch.toLowerCase().trim(); - return safeRows.filter((r) => - [r.tenantname, r.locationname, String(r.tenantid || ''), String(r.locationid || '')] - .filter(Boolean) - .some((field) => String(field).toLowerCase().includes(q)) - ); - }, [safeRows, debouncedSearch]); - - // KPIs + grand total derived directly from the loaded summary. - const stats = useMemo(() => { - return filteredRows.reduce( - (acc, row) => { - acc.totalOrders += Number(row.totalorders) || 0; - acc.orderPend += Number(row.Orderspending) || 0; - acc.orderComplete += Number(row.orderscompleted) || 0; - acc.orderCancel += Number(row.orderscancelled) || 0; - acc.deliPend += Number(row.deliveriespending) || 0; - acc.deliComplete += Number(row.deliveriescompleted) || 0; - acc.deliCancel += Number(row.deliveriescancelled) || 0; - acc.collection += Number(row.collectionamt) || 0; - acc.kms += Number(row.kms) || 0; - acc.cumulativekms += Number(row.cumulativekms) || 0; - acc.amount += Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); - return acc; - }, - { - totalOrders: 0, - orderPend: 0, - orderComplete: 0, - orderCancel: 0, - deliPend: 0, - deliComplete: 0, - deliCancel: 0, - collection: 0, - kms: 0, - cumulativekms: 0, - amount: 0 - } - ); - }, [filteredRows]); - - // ==============================|| per-tenant rider breakdown ||============================== // - const getuserreportsummary = async (tenantId) => { - setLoading(true); - try { - const res = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getuserreportsummary/?tenantid=${tenantId}&fromdate=${startdate}&todate=${enddate}` - ); - setRidersdata(Array.isArray(res.data?.details) ? res.data.details : []); - } catch (err) { - OpenToast(err?.message || 'Failed to load rider breakdown', 'error', 2000); - setRidersdata([]); - } finally { - setLoading(false); - } - }; - - // ==============================|| per-location rider breakdown ||============================== // - const getriderlocationsummary = async (locId) => { - setLoading(true); - try { - const res = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getriderlocationsummary/?tenantid=${tenantid}&locationid=${locId}&fromdate=${startdate}&todate=${enddate}` - ); - setRidersdata(Array.isArray(res.data?.details) ? res.data.details : []); - } catch (err) { - OpenToast(err?.message || 'Failed to load rider breakdown', 'error', 2000); - setRidersdata([]); - } finally { - setLoading(false); - } - }; - - if (isErrorReports) console.warn('ordersSummary error:', reportsError?.message); - - const KPI_META = [ - { - key: 'orders', - label: 'Total Orders', - color: BRAND, - icon: MdOutlineLocalShipping, - value: stats.totalOrders - }, - { - key: 'pending', - label: 'Orders Pending', - color: '#f59e0b', - icon: MdOutlinePendingActions, - value: stats.orderPend - }, - { - key: 'completed', - label: 'Orders Completed', - color: '#10b981', - icon: MdOutlineCheckCircle, - value: stats.orderComplete - }, - { - key: 'amount', - label: 'Total Amount', - color: '#0ea5e9', - icon: MdOutlineCurrencyRupee, - value: formatNumberToRupees(stats.amount) - } - ]; - - const isLocationGroup = Boolean(tenantid); - - return ( - <> - {(loading || isLoadingReports) && } - - {/* ============================================= || Header || ============================================= */} - } - placeholder="Select Zone" - paperComponent={SoftPaper} - sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }} - /> - } - /> - - {/* ============================================= || KPI Cards || ============================================= */} - - {KPI_META.map((item) => { - const Icon = item.icon; - return ( - - } - color={item.color} - loading={isLoadingReports} - /> - - ); - })} - - - {/* ============================================= || Filter bar || ============================================= */} - - - - - setOpen(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')}` } - }} - > - - Orders · {datestatus} · {dayjs(startdate).format('DD/MM/YY')} – {dayjs(enddate).format('DD/MM/YY')} - - - - option?.tenantname || option?.label || ''} - isOptionEqualToValue={(opt, val) => opt?.tenantid === val?.tenantid} - PaperComponent={SoftPaper} - sx={{ minWidth: { xs: '100%', sm: 220 }, ...pillFieldSx(BRAND) }} - onOpen={(event) => { - if (!appId) { - event.preventDefault(); - OpenToast('Please select a Zone first!', 'warning', 3000); - setTimeout(() => locationRef.current?.focus(), 0); - } - }} - onChange={(e, val, reason) => { - if (reason === 'clear' || !val) { - setTenantid(0); - setTenantValue(null); - setLocationid(0); - setLocationValue(null); - } else { - setTenantid(val?.tenantid || 0); - setTenantValue(val); - setLocationid(val?.locationid || 0); - setLocationValue(null); - } - }} - renderInput={(params) => ( - - - - - {params.InputProps.startAdornment} - - ) - }} - /> - )} - /> - - - option?.locationname ? `${option.locationname}${option.suburb ? ` (${option.suburb})` : ''}` : '' - } - isOptionEqualToValue={(opt, val) => opt?.locationid === val?.locationid} - PaperComponent={SoftPaper} - sx={{ minWidth: { xs: '100%', sm: 220 }, ...pillFieldSx('#0ea5e9') }} - onOpen={(event) => { - if (!appId && !tenantid) { - event.preventDefault(); - OpenToast('Please select Zone and Tenant first!', 'warning', 3000); - setTimeout(() => locationRef.current?.focus(), 0); - } else if (!tenantid) { - event.preventDefault(); - OpenToast('Please select a Tenant first!', 'warning', 3000); - setTimeout(() => tenantRef.current?.focus(), 0); - } - }} - onChange={(e, val, reason) => { - if (reason === 'clear' || !val) { - setLocationid(0); - setLocationValue(null); - } else { - setLocationid(val?.locationid || 0); - setLocationValue(val); - } - }} - renderInput={(params) => ( - - - - - {params.InputProps.startAdornment} - - ) - }} - /> - )} - /> - - - - - - - - - {/* ============================================= || Table || ============================================= */} - - {isMobile ? ( - <> - {isLoadingReports && ( - - {[0, 1, 2, 3].map((i) => ( - - - - ))} - - )} - - {!isLoadingReports && filteredRows.length === 0 && ( - - - - - - No {isLocationGroup ? 'locations' : 'tenants'} to show - - - {debouncedSearch - ? 'Try a different keyword.' - : isErrorReports - ? 'Something went wrong fetching the summary. Try a different filter.' - : 'Pick a zone, tenant, or date range to load the summary.'} - - - )} - - {!isLoadingReports && filteredRows.length > 0 && ( - - {filteredRows.map((row, index) => { - const rowKey = isLocationGroup ? row.locationname : row.tenantname; - const isOpen = openRow === rowKey; - const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); - const rowAccent = isLocationGroup ? '#0ea5e9' : BRAND; - - return ( - - - {isLocationGroup ? : } - - - - {(isLocationGroup ? row.locationname : row.tenantname) || '—'} - - - ID #{isLocationGroup ? row.locationid : row.tenantid} - - - - { - const isOpening = !isOpen; - setOpenRow(isOpening ? rowKey : null); - if (!isOpening) return; - setRidersdata([]); - if (isLocationGroup) { - getriderlocationsummary(row.locationid); - } else { - getuserreportsummary(row.tenantid); - } - }} - sx={{ - flexShrink: 0, - bgcolor: isOpen ? BRAND : soft(BRAND), - color: isOpen ? '#fff' : BRAND, - border: `1px solid ${edge(BRAND)}`, - '&:hover': { bgcolor: BRAND, color: '#fff' } - }} - > - {isOpen ? : } - - - - } - > - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - } - label={Number(row.collectionamt || 0).toFixed(2)} - tooltip="Collection Amount" - minWidth={90} - /> - - - } - label={formatNumberToRupees(amount).replace('₹', '').trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - } - label={`${Number(row.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - - - } - label={`${Number(row.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - - {/* Collapsible rider breakdown — mobile */} - - - - - - - - Rider Breakdown - - - - {!loading && (!ridersdata || ridersdata.length === 0) ? ( - - No rider activity for this row. - - ) : ( - - {ridersdata.map((sub, sidx) => { - const subAmount = Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0); - return ( - - - - - - - - {`${sub.firstname || ''} ${sub.lastname || ''}`.trim() || '—'} - - - {sub.ridercontact || `ID #${sub.userid}`} - - - - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - } - label={Number(sub.collectionamt || 0).toFixed(2)} - tooltip="Collection" - minWidth={80} - /> - - - } - label={`${Number(sub.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - - - } - label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - } - label={formatNumberToRupees(subAmount).replace('₹', '').trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - - ); - })} - - )} - - - - ); - })} - - )} - - ) : ( - - - - - # - {isLocationGroup ? 'Location' : 'Tenant'} - - All - - - Orders - - - Deliveries - - - Collection - - - KMS / Actual - - - Amount - - - Action - - - - - Pending - - - Cancelled - - - Completed - - - Pending - - - Cancelled - - - Completed - - - - - - {isLoadingReports && } - - {!isLoadingReports && filteredRows.length === 0 && ( - - - - - - - - No {isLocationGroup ? 'locations' : 'tenants'} to show - - - {debouncedSearch - ? 'Try a different keyword.' - : isErrorReports - ? 'Something went wrong fetching the summary. Try a different filter.' - : 'Pick a zone, tenant, or date range to load the summary.'} - - - - - )} - - {filteredRows.map((row, index) => { - const rowKey = isLocationGroup ? row.locationname : row.tenantname; - const isOpen = openRow === rowKey; - const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); - - return ( - - - - - {String(index + 1).padStart(2, '0')} - - - - - - {isLocationGroup ? : } - - - - {(isLocationGroup ? row.locationname : row.tenantname) || '—'} - - - ID #{isLocationGroup ? row.locationid : row.tenantid} - - - - - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - } /> - - - - } - label={Number(row.collectionamt || 0).toFixed(2)} - tooltip="Collection Amount" - minWidth={90} - /> - - - - - } - label={`${Number(row.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - } - label={`${Number(row.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - - - } - label={formatNumberToRupees(amount).replace('₹', '').trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - - - { - const isOpening = !isOpen; - setOpenRow(isOpening ? rowKey : null); - if (!isOpening) return; - setRidersdata([]); - if (isLocationGroup) { - getriderlocationsummary(row.locationid); - } else { - getuserreportsummary(row.tenantid); - } - }} - sx={{ - bgcolor: isOpen ? BRAND : soft(BRAND), - color: isOpen ? '#fff' : BRAND, - border: `1px solid ${edge(BRAND)}`, - '&:hover': { bgcolor: BRAND, color: '#fff' } - }} - > - {isOpen ? : } - - - - - - {/* ============================================= || Collapsible rider breakdown || ============================================= */} - {isOpen && ( - - - - - - - - - - - Rider Breakdown · {isLocationGroup ? row.locationname : row.tenantname} - - - -
- - - # - Rider - Orders - Deliveries - Pending - Cancelled - Completed - Collection - KMS / Actual - Amount - - - - {loading && } - {!loading && (!ridersdata || ridersdata.length === 0) ? ( - - - - No rider activity for this row. - - - - ) : ( - ridersdata.map((sub, sidx) => { - const subAmount = Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0); - return ( - - - - {String(sidx + 1).padStart(2, '0')} - - - - - - - - - - {`${sub.firstname || ''} ${sub.lastname || ''}`.trim() || '—'} - - - {sub.ridercontact || `ID #${sub.userid}`} - - - - - - } /> - - - } /> - - - } - /> - - - } /> - - - } - /> - - - } - label={Number(sub.collectionamt || 0).toFixed(2)} - tooltip="Collection" - minWidth={80} - /> - - - - } - label={`${Number(sub.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - } - label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - - } - label={formatNumberToRupees(subAmount).replace('₹', '').trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - ); - }) - )} - -
-
- - - - - )} - - ); - })} - - - - )} - - {/* ============================================= || Total Bar || ============================================= */} - {filteredRows.length > 0 && ( - <> - - - - - Grand Total - - - - - - - - - )} -
- - {/* ============================================= || Date Filter Dialog || ============================================= */} - setOpen(false)} - onSelect={(range) => { - setStartdate(range.startDate); - setEnddate(range.endDate); - setDatestatus(range.label); - }} - /> - - ); -} diff --git a/src/pages/nearle/reports/profitability.js b/src/pages/nearle/reports/profitability.js deleted file mode 100644 index aa320f7..0000000 --- a/src/pages/nearle/reports/profitability.js +++ /dev/null @@ -1,799 +0,0 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import PropTypes from 'prop-types'; -import { useInfiniteQuery } from '@tanstack/react-query'; -import { - Avatar, - Box, - Chip, - Grid, - Paper, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Tooltip, - Typography, - useMediaQuery, - useTheme -} from '@mui/material'; -import { - MdMyLocation, - MdCalendarMonth, - MdPerson, - MdOutlineLocalShipping, - MdOutlineCurrencyRupee, - MdStraighten, - MdPayments, - MdRoute, - MdTrendingUp, - MdTrendingDown -} from 'react-icons/md'; - -import dayjs from 'dayjs'; -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); - -import { fetchDeliveries } from 'pages/api/api'; -import Loader from 'components/Loader'; -import DateFilterDialog from 'components/DateFilterDialog'; -import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; -import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; -import PageHeader from 'components/nearle_components/PageHeader'; -import StatCard from 'components/nearle_components/StatCard'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; - -const DT = { - radiusPill: 999, - radiusCard: 14, - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', - shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)', - shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; - -const aColor = (c, suffix) => `${c}${suffix}`; -const soft = (c) => aColor(c, '18'); -const tint = (c) => aColor(c, '08'); -const edge = (c) => aColor(c, '55'); -const ring = (c) => aColor(c, '26'); - -const BRAND = '#C01227'; - -const SoftPaper = (props) => ( - -); - -SoftPaper.propTypes = { - children: PropTypes.node -}; - -const AccentAvatar = ({ color, selected, size = 24, children }) => ( - - {children} - -); - -AccentAvatar.propTypes = { - color: PropTypes.string.isRequired, - selected: PropTypes.bool, - size: PropTypes.number, - children: PropTypes.node -}; - -const MetricPill = ({ color, icon, label, tooltip, minWidth = 80 }) => ( - - - {icon} - {label} - - -); - -MetricPill.propTypes = { - color: PropTypes.string.isRequired, - icon: PropTypes.node, - label: PropTypes.string.isRequired, - tooltip: PropTypes.string, - minWidth: PropTypes.number -}; - -const BATCHES = [ - { id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 }, - { id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12.5 }, - { id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 } -]; - -const getBatchForHour = (h, batches = BATCHES) => { - for (const b of batches) { - if (h >= b.startHour && h < b.endHour) return b.id; - } - return null; -}; - -const getRowBatch = (r, batches = BATCHES) => { - const t = r?.assigntime; - if (!t) return null; - const str = String(t).trim(); - if (/^\d{4}-\d{2}-\d{2}$/.test(str)) return null; - const d = dayjs(t); - if (!d.isValid()) return null; - return getBatchForHour(d.hour() + d.minute() / 60, batches); -}; - -function formatNumberToRupees(value) { - return new Intl.NumberFormat('en-IN', { - style: 'currency', - currency: 'INR', - minimumFractionDigits: 2 - }).format(Number(value) || 0); -} - -export default function ProfitabilityReport() { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - - const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); - const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); - const [locaName, setLocoName] = useState('All'); - const [open, setOpen] = useState(false); - const [datestatus, setDatestatus] = useState('Today'); - const [appId, setAppId] = useState(0); - - const [searchword, setSearchword] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - - const liveUserid = typeof window !== 'undefined' ? localStorage.getItem('userid') || 0 : 0; - - // Load slots configuration from localStorage to match Dispatch page edits - const customBatches = useMemo(() => { - if (typeof window === 'undefined') return BATCHES; - try { - const raw = window.localStorage.getItem('dispatch.slots.v9'); - if (!raw) return BATCHES; - const parsed = JSON.parse(raw); - if (!Array.isArray(parsed) || parsed.length !== BATCHES.length) return BATCHES; - return parsed.map((s, i) => { - const id = s.id || `slot-${i + 1}`; - const startHour = Number(s.startHour) || 0; - const endHour = Number(s.endHour) || 24; - return { - id, - name: s.name || BATCHES.find((b) => b.id === id)?.name || `Slot ${i + 1}`, - startHour, - endHour - }; - }); - } catch (e) { - return BATCHES; - } - }, []); - - // Fetch all deliveries for the selected date range and zone - const { - data: deliveriesData, - isLoading: isLoadingDeliveries, - fetchNextPage, - hasNextPage, - isFetchingNextPage - } = useInfiniteQuery({ - queryKey: ['fetchdeliveries', appId, liveUserid, 'all', startdate, enddate, 2000, '', 0, 0, 0], - queryFn: fetchDeliveries, - getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined, - refetchOnWindowFocus: false - }); - - // Auto-page through all results - useEffect(() => { - if (hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - }, [hasNextPage, isFetchingNextPage, fetchNextPage]); - - // Flatten and deduplicate deliveries by orderid - const liveRows = useMemo(() => { - const all = (deliveriesData?.pages || []).flatMap((p) => p.rows || []); - const seen = new Set(); - const out = []; - for (const r of all) { - const key = r.orderid != null ? String(r.orderid) : null; - if (key && seen.has(key)) continue; - if (key) seen.add(key); - out.push(r); - } - return out; - }, [deliveriesData]); - - // Group deliveries by rider - const ridersList = useMemo(() => { - const riderMap = {}; - liveRows.forEach((r) => { - const key = String(r.userid || r.rider_id || ''); - if (!key || key === 'unassigned' || key === '0') return; - if (!riderMap[key]) { - riderMap[key] = { - id: key, - riderName: r.ridername || r.rider_name || r.username || `Rider ${key}`, - orders: [] - }; - } - if (!riderMap[key].orders.some((existing) => existing.orderid === r.orderid)) { - riderMap[key].orders.push(r); - } - }); - - return Object.values(riderMap) - .map((r) => ({ - ...r, - orders: [...r.orders].sort((a, b) => { - const tA = a.trip_number || 1; - const tB = b.trip_number || 1; - if (tA !== tB) return tA - tB; - return (a.step || 0) - (b.step || 0); - }) - })) - .sort((a, b) => b.orders.length - a.orders.length); - }, [liveRows]); - - // Calculate profitability metrics for all riders - const stats = useMemo(() => { - let activeRiders = 0; - let totalOrders = 0; - let totalRevenue = 0; - let totalCost = 0; - let profitableRiders = 0; - let lossRiders = 0; - let totalKms = 0; - - const list = ridersList - .map((r) => { - let rRevenue = 0; - let rKms = 0; - const slotsByDate = {}; - let ordersInSlots = 0; - - r.orders.forEach((o) => { - const slot = getRowBatch(o, customBatches); - if (!slot) return; - - const oKms = parseFloat(o.riderkms || 0); - rKms += oKms; - rRevenue += oKms <= 8 ? 30 : 30 + (oKms - 8) * 6; - - const dateStr = o.assigntime - ? dayjs(o.assigntime).format('YYYY-MM-DD') - : o.deliverydate - ? dayjs(o.deliverydate).format('YYYY-MM-DD') - : null; - if (!dateStr) return; - - if (!slotsByDate[dateStr]) { - slotsByDate[dateStr] = new Set(); - } - slotsByDate[dateStr].add(slot); - ordersInSlots++; - }); - - if (ordersInSlots === 0) { - return null; - } - - // Sum unique slots per day, capping at 3 slots max per day - let slotCount = 0; - Object.values(slotsByDate).forEach((set) => { - slotCount += Math.min(set.size, 3); - }); - - const rVarCost = rKms * 2.5; - const rFixedCost = slotCount * (500 / 3); - const rTotalCost = rVarCost + rFixedCost; - const rNet = rRevenue - rTotalCost; - const rMargin = rRevenue > 0 ? (rNet / rRevenue) * 100 : 0; - - if (rNet >= 0) { - profitableRiders++; - } else { - lossRiders++; - } - - totalOrders += ordersInSlots; - totalRevenue += rRevenue; - totalCost += rTotalCost; - totalKms += rKms; - activeRiders++; - - return { - ...r, - kms: rKms, - revenue: rRevenue, - varCost: rVarCost, - fixedCost: rFixedCost, - totalCost: rTotalCost, - net: rNet, - margin: rMargin - }; - }) - .filter(Boolean); - - const totalNet = totalRevenue - totalCost; - const totalMargin = totalRevenue > 0 ? (totalNet / totalRevenue) * 100 : 0; - - return { - activeRiders, - totalOrders, - totalRevenue, - totalCost, - totalNet, - totalMargin, - profitableRiders, - lossRiders, - totalKms, - enrichedRiders: list - }; - }, [ridersList, customBatches]); - - // Filter riders by search query - const filteredRiders = useMemo(() => { - if (!stats?.enrichedRiders || !Array.isArray(stats.enrichedRiders)) return []; - const baseList = stats.enrichedRiders.filter(Boolean); - if (!debouncedSearch) return baseList; - const q = debouncedSearch.toLowerCase().trim(); - return baseList.filter( - (r) => r && [r.riderName, String(r.id)].filter(Boolean).some((field) => String(field).toLowerCase().includes(q)) - ); - }, [stats?.enrichedRiders, debouncedSearch]); - - const KPI_META = [ - { - key: 'riders', - label: 'Riders Active', - color: BRAND, - icon: MdPerson, - value: stats?.activeRiders ?? 0, - detail: `${stats?.profitableRiders ?? 0} in profit · ${stats?.lossRiders ?? 0} at loss` - }, - { - key: 'kms', - label: 'Total Distance', - color: '#10b981', - icon: MdStraighten, - value: `${(stats?.totalKms ?? 0).toFixed(1)} km`, - detail: 'Cumulative travel distance' - }, - { - key: 'revenue', - label: 'Slot Revenue', - color: '#0ea5e9', - icon: MdOutlineLocalShipping, - value: formatNumberToRupees(stats?.totalRevenue ?? 0), - detail: `From ${stats?.totalOrders ?? 0} orders` - }, - { - key: 'cost', - label: 'Slot Cost', - color: '#f59e0b', - icon: MdPayments, - value: formatNumberToRupees(stats?.totalCost ?? 0), - detail: 'Fixed + variable' - }, - { - key: 'net', - label: 'Slot Net', - color: (stats?.totalNet ?? 0) >= 0 ? '#10b981' : '#ef4444', - icon: (stats?.totalNet ?? 0) >= 0 ? MdTrendingUp : MdTrendingDown, - value: `${(stats?.totalNet ?? 0) >= 0 ? '+' : ''}${formatNumberToRupees(stats?.totalNet ?? 0)}`, - detail: `${(stats?.totalRevenue ?? 0) > 0 ? ((stats?.totalNet ?? 0) / (stats?.totalRevenue ?? 1) >= 0 ? '+' : '') : ''}${( - stats?.totalMargin ?? 0 - ).toFixed(0)}% margin` - } - ]; - - return ( - <> - {(isLoadingDeliveries || isFetchingNextPage) && } - - {/* Page Header */} - } - placeholder="Select Zone" - paperComponent={SoftPaper} - sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }} - /> - } - /> - - {/* KPI Cards Grid */} - - {KPI_META.map((item) => { - const Icon = item.icon; - return ( - - } - color={item.color} - loading={isLoadingDeliveries} - /> - - {item.detail} - - - ); - })} - - - {/* Filter Bar (date + search) */} - - - - - - - - - Profitability Overview · {datestatus} - - - {filteredRiders.length} riders · {stats.profitableRiders} profitable · {stats.lossRiders} at loss - - - - setOpen(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, - ml: 1, - 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')} - - - - - - - - - - {/* Table & Mobile List Container */} - - {isMobile ? ( - - {!filteredRiders || filteredRiders.length === 0 ? ( - - - - - - No riders to show - - - ) : ( - filteredRiders.map((row, index) => { - if (!row) return null; - const isProfit = (row.net ?? 0) >= 0; - return ( - - - - - - - {row.riderName} - - - ID #{row.id} - - - - } - > - - - - - - - - - - - - ); - }) - )} - - ) : ( - - - - - # - Rider - Orders - Rider KMs - Revenue - Fixed Cost - Variable Cost - Total Cost - Net Profit - Margin - - - - {!filteredRiders || filteredRiders.length === 0 ? ( - - - - - - - - No riders to show - - - - - ) : ( - filteredRiders.map((row, index) => { - if (!row) return null; - const isProfit = (row.net ?? 0) >= 0; - return ( - - - - {String(index + 1).padStart(2, '0')} - - - - - - - - - - {row.riderName} - - - ID #{row.id} - - - - - - - {row.orders.length} - - - - } label={`${row.kms.toFixed(2)} km`} tooltip="KMS" /> - - - } - label={formatNumberToRupees(row.revenue).replace('₹', '').trim()} - tooltip="Revenue" - /> - - - } - label={formatNumberToRupees(row.fixedCost).replace('₹', '').trim()} - tooltip="Fixed Cost" - /> - - - } - label={formatNumberToRupees(row.varCost).replace('₹', '').trim()} - tooltip="Variable Cost" - /> - - - } - label={formatNumberToRupees(row.totalCost).replace('₹', '').trim()} - tooltip="Total Cost" - /> - - - : } - label={`${isProfit ? '+' : ''}${formatNumberToRupees(row.net).replace('₹', '').trim()}`} - tooltip="Net Profit" - /> - - - - - - ); - }) - )} - -
-
- )} -
- - {/* Date Filter Dialog */} - setOpen(false)} - onSelect={(range) => { - setStartdate(range.startDate); - setEnddate(range.endDate); - setDatestatus(range.label); - }} - /> - - ); -} diff --git a/src/pages/nearle/reports/ridersLogs.js b/src/pages/nearle/reports/ridersLogs.js deleted file mode 100644 index 2fa714f..0000000 --- a/src/pages/nearle/reports/ridersLogs.js +++ /dev/null @@ -1,355 +0,0 @@ -import React, { useState, useEffect, Fragment } from 'react'; -import { - Box, - Drawer, - IconButton, - Toolbar, - Typography, - AppBar, - useMediaQuery, - Divider, - List, - ListItem, - ListItemText, - useTheme, - ListItemAvatar, - Stack, - Button, - Checkbox, - Skeleton -} from '@mui/material'; - -import MenuIcon from '@mui/icons-material/Menu'; -import SearchBar from 'components/nearle_components/SearchBar'; -import { useQuery } from '@tanstack/react-query'; -import { fetchRidersLogs } from 'pages/api/api'; -import RiderLocationMap from './RiderLocationMap'; -import MainCard from 'components/MainCard'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; -import dayjs from 'dayjs'; - -import error500 from 'assets/images/maintenance/Error500.png'; - -const drawerWidth = 350; - -const RidersLogs = () => { - const theme = useTheme(); - const isDesktop = useMediaQuery('(min-width:900px)'); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const [open, setOpen] = useState(false); - const [selectedRiders, setSelectedRiders] = useState([]); - const [riderSearch, setRiderSearch] = useState(''); - const appId = 1; - const { - data: riders, - isLoading: ridersIsLoading, - isFetching: riderIsFetching, - refetch: riderLogsRefetch, - error: riderLogsError - } = useQuery({ - queryKey: [appId, dayjs().format('YYYY-MM-DD'), riderSearch], - queryFn: fetchRidersLogs, - refetchInterval: 5 * 60 * 1000 - }); - - useEffect(() => { - // const sortedRiders = riders?.sort((a, b) => a.firstname.localeCompare(b.firstname)); - setSelectedRiders(riders); - }, [riders]); - - useEffect(() => { - console.log('selectedRiders', selectedRiders); - }, [selectedRiders]); - - useEffect(() => { - setOpen(isDesktop); - }, [isDesktop]); - - return ( - - - {/* Drawer */} - !isDesktop && setOpen(false)} - ModalProps={{ keepMounted: true }} - sx={{ - '& .MuiDrawer-paper': { - width: isMobile ? '100vw' : drawerWidth, - maxWidth: isMobile ? '100vw' : drawerWidth, - position: 'absolute', - left: 0, - top: 0, - height: '100%', - overflowY: 'auto', - transition: 'transform 0.35s ease-in-out', - zIndex: 13 - } - }} - > - {/* Search */} - - setRiderSearch(e.target.value)} - sx={{ - height: 60, - bgcolor: 'white', - '& .MuiOutlinedInput-notchedOutline': { - borderBottom: '1px solid', - borderColor: theme.palette.secondary.light - } - }} - /> - - - - { - if (e.target.checked) { - setSelectedRiders(riders); - } - }} - /> - - - - - - - {/* Rider List */} - - {/* Individuals */} - {ridersIsLoading || riderIsFetching - ? Array.from({ length: 10 }).map((_, index) => ( - - - - - - - } - secondary={} - /> - - - - - - - - - - )) - : !isMobile && - riders?.map((row) => { - return ( - - - - {row.userid} - - - {dayjs(row.logdate).format('DD/MM/YYYY hh:mm A')} - - - } - > - - { - if (e.target.checked) { - // SELECT ONE RIDER - setSelectedRiders([row]); - } else { - // UNCHECK -> SELECT ALL - setSelectedRiders(riders); - } - }} - /> - - - - {row.username?.slice(0, 25) || ''} - {row.username?.length > 25 && '...'} - - {/* {row.status === 'active' && } */} - - } - secondary={ - - {row.contactno || '##########'} - - } - /> - - - - - ); - })} - - - {/* Mobile: rider rows rendered as app-style cards (same selection behaviour) */} - {isMobile && !ridersIsLoading && !riderIsFetching && ( - - {riders?.map((row) => { - const isActive = row.status == 'active'; - const isSelected = selectedRiders?.length === 1 && selectedRiders[0]?.userid === row?.userid; - return ( - - { - if (e.target.checked) { - setSelectedRiders([row]); - } else { - setSelectedRiders(riders); - } - }} - /> - - - {row.username?.slice(0, 25) || ''} - {row.username?.length > 25 && '...'} - - - {row.contactno || '##########'} - - - - } - > - - - - {row.userid} - - - - - - - ); - })} - - )} - - - {/* AppBar */} - - - - - setOpen(!open)}> - - - - - Riders Locations - - - - - - - - - {/* Map */} - - {(ridersIsLoading || riderIsFetching) && ( - - {/* */} - - - )} - - {selectedRiders?.length > 0 && } - {riderLogsError && ( - - mantis - - )} - - - - ); -}; - -export default RidersLogs; diff --git a/src/pages/nearle/reports/ridersSummary.js b/src/pages/nearle/reports/ridersSummary.js deleted file mode 100644 index 7841cf7..0000000 --- a/src/pages/nearle/reports/ridersSummary.js +++ /dev/null @@ -1,1156 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import axios from 'axios'; -import { useQuery } from '@tanstack/react-query'; - -// material-ui -import { - Avatar, - Box, - Chip, - Collapse, - Dialog, - DialogContent, - Divider, - Grid, - IconButton, - Paper, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Tooltip, - Typography, - useMediaQuery, - useTheme -} from '@mui/material'; -import { - MdDirectionsBike, - MdMyLocation, - MdCalendarMonth, - MdLocalShipping, - MdCheckCircle, - MdHourglassEmpty, - MdCancel, - MdCurrencyRupee, - MdStraighten, - MdMap, - MdExpandMore, - MdExpandLess, - MdGroups, - MdPerson, - MdOutlineLocalShipping, - MdOutlineCheckCircle, - MdOutlineCurrencyRupee -} from 'react-icons/md'; - -import dayjs from 'dayjs'; -var utc = require('dayjs/plugin/utc'); -dayjs.extend(utc); - -import { fetchRidersSummary } from 'pages/api/api'; -import Loader from 'components/Loader'; -import DateFilterDialog from 'components/DateFilterDialog'; -import LocationAutocomplete from 'components/nearle_components/LocationAutocomplete'; -import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; -import PageHeader from 'components/nearle_components/PageHeader'; -import StatCard from 'components/nearle_components/StatCard'; -import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; -import RidersRoutes from './RidersRoutes'; -import { OpenToast } from 'components/third-party/OpenToast'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; - -// ============================================================================ -// Design tokens — shared with deliveries / tenants / customers / pricing / -// orders-details pages. -// ============================================================================ -const DT = { - radiusPill: 999, - radiusCard: 14, - shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', - shadowMd: '0 1px 3px rgba(15, 23, 42, 0.06)', - shadowPop: '0 12px 32px rgba(15, 23, 42, 0.12)', - textPrimary: '#0f172a', - textSecondary: '#64748b', - textMuted: '#94a3b8', - borderSubtle: '#e2e8f0', - divider: '#f1f5f9', - surface: '#ffffff', - surfaceAlt: '#f8fafc' -}; -const a = (c, suffix) => `${c}${suffix}`; -const tint = (c) => a(c, '08'); -const soft = (c) => a(c, '18'); -const ring = (c) => a(c, '26'); -const edge = (c) => a(c, '55'); - -const BRAND = '#C01227'; -const BRAND_LIGHT = '#D35968'; - -const SoftPaper = (props) => ( - -); - -const AccentAvatar = ({ color, selected, size = 24, children }) => ( - - {children} - -); - -// Pill used in cells for numeric / km / amount metrics. -const MetricPill = ({ color, icon, label, tooltip, minWidth = 80 }) => ( - - - {icon} - {label} - - -); - -// Coloured numeric cell — non-zero values get a soft red badge to draw the eye, -// preserving the legacy "red when present" affordance. -const CountCell = ({ value, color = '#ef4444', icon }) => { - const n = Number(value) || 0; - if (n === 0) { - return ( - - 0 - - ); - } - return ( - - {icon} - {n} - - ); -}; - -function formatNumberToRupees(value) { - return new Intl.NumberFormat('en-IN', { - style: 'currency', - currency: 'INR', - minimumFractionDigits: 2 - }).format(Number(value) || 0); -} - -// ==============================|| Riders Summary ||============================== // - -export default function RidersSummary() { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - - const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); - const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); - const [locaName, setLocoName] = useState('All'); - const [open, setOpen] = useState(false); - const [datestatus, setDatestatus] = useState('Today'); - const [tenantData, setTenantData] = useState([]); - const [openRow, setOpenRow] = useState(null); - const [appId, setAppId] = useState(0); - const [loading, setLoading] = useState(false); - const [mapOpen, setMapOpen] = useState(false); - const [logDetails, setLogDetails] = useState(null); - const [selectedRider, setSelectedRider] = useState(null); - const [routeLoading, setRouteLoading] = useState(false); - const [searchword, setSearchword] = useState(''); - const [debouncedSearch, setDebouncedSearch] = useState(''); - - // ==============================|| fetchRidersSummary ||============================== // - const { isLoading: isLoadingReports, data: rows } = useQuery({ - queryKey: ['ridersummary', appId, startdate, enddate], - queryFn: fetchRidersSummary - }); - - // Client-side filter across rider name + id. - const filteredRows = useMemo(() => { - if (!rows) return []; - if (!debouncedSearch) return rows; - const q = debouncedSearch.toLowerCase().trim(); - return rows.filter((r) => - [`${r.firstname || ''} ${r.lastname || ''}`, String(r.userid)] - .filter(Boolean) - .some((field) => String(field).toLowerCase().includes(q)) - ); - }, [rows, debouncedSearch]); - - // Aggregate KPIs from the loaded summary. - const stats = useMemo(() => { - if (!rows || rows.length === 0) return { riders: 0, orders: 0, delivered: 0, amount: 0 }; - return rows.reduce( - (acc, r) => { - acc.riders += 1; - acc.orders += Number(r.totalorders) || 0; - acc.delivered += Number(r.delivered) || 0; - acc.amount += Math.max(Number(r.charges) || 0, Number(r.deliveryamt) || 0); - return acc; - }, - { riders: 0, orders: 0, delivered: 0, amount: 0 } - ); - }, [rows]); - - // ==============================|| per-rider tenant breakdown ||============================== // - const fetchTenantSummary = async (riderUserid) => { - setLoading(true); - try { - const tenantRes = await axios.get( - `${process.env.REACT_APP_URL}/deliveries/getreportsummary/?&fromdate=${startdate}&todate=${enddate}&userid=${riderUserid}` - ); - setTenantData(tenantRes.data.details); - } catch (error) { - console.log('tenantRes', error); - } finally { - setLoading(false); - } - }; - - // ==============================|| rider planned route (for map) ||============================== // - // Pulls every delivery the rider was assigned over the page's date range, then - // emits an ordered waypoint list sorted by `step` (the planning sequence). The - // map dialog renders this as the rider's PLANNED route — the path the - // optimizer told them to follow — not their actual GPS trail. - const getuserdeliverylogs = async (userid) => { - setRouteLoading(true); - try { - // /deliveries/getdeliveries treats applocationid=0 differently from a - // real location id — when appId===0 ("All") the backend expects the - // logged-in operator's userid via appuserid instead. Mirrors the - // branching in api.js#fetchDeliveries. - const loggedInUserId = typeof window !== 'undefined' ? localStorage.getItem('userid') || 0 : 0; - const scopeParam = appId === 0 - ? `appuserid=${loggedInUserId}` - : `applocationid=${appId}`; - const url = - `${process.env.REACT_APP_URL}/deliveries/getdeliveries/` + - `?${scopeParam}` + - `&status=all` + - `&fromdate=${startdate}` + - `&todate=${enddate}` + - `&pageno=1` + - `&pagesize=200` + - `&keyword=` + - `&tenantid=` + - `&locationid=` + - `&userid=${userid}`; - const response = await axios.get(url); - const rowsRaw = response?.data?.details || []; - const toNum = (v) => { - const n = Number(v); - return Number.isFinite(n) ? n : null; - }; - const planned = rowsRaw - .map((o) => { - const dropLat = toNum(o.droplat ?? o.deliverylat); - const dropLng = toNum(o.droplon ?? o.deliverylong); - const pickLat = toNum(o.pickuplat ?? o.pickuplatitude); - const pickLng = toNum(o.pickuplon ?? o.pickuplong ?? o.picklongitude); - if (dropLat == null || dropLng == null) return null; - return { - step: Number(o.step) || 0, - orderid: o.orderid, - deliveryid: o.deliveryid, - customer: o.deliverycustomer || o.customername || `Order ${o.orderid}`, - address: o.deliveryaddress || o.deliverysuburb || '', - dropLat, - dropLng, - pickLat: pickLat ?? null, - pickLng: pickLng ?? null, - // Expected delivery clock — used as a label under the step pin so - // the operator can sanity-check sequencing without clicking each - // marker. - expectedTime: o.expecteddeliverytime || null - }; - }) - .filter(Boolean) - .sort((a, b) => a.step - b.step); - setLogDetails(planned); - } catch (err) { - OpenToast(err?.message, 'error', 2000); - setLogDetails([]); - } finally { - setRouteLoading(false); - } - }; - - // Total Amount sum (preserved from legacy bottom bar). - const total = useMemo(() => { - if (!rows) return 0; - return rows.reduce((sum, row) => sum + (Number(row.deliveryamt) || 0), 0); - }, [rows]); - - const KPI_META = [ - { key: 'riders', label: 'Active Riders', color: BRAND, icon: MdDirectionsBike, value: stats.riders }, - { key: 'orders', label: 'Total Orders', color: '#0ea5e9', icon: MdOutlineLocalShipping, value: stats.orders }, - { key: 'delivered', label: 'Delivered', color: '#10b981', icon: MdOutlineCheckCircle, value: stats.delivered }, - { key: 'amount', label: 'Total Amount', color: '#f59e0b', icon: MdOutlineCurrencyRupee, value: formatNumberToRupees(total) } - ]; - - return ( - <> - {(isLoadingReports || loading) && } - - {/* ============================================= || Header || ============================================= */} - } - placeholder="Select Zone" - paperComponent={SoftPaper} - sx={{ width: { xs: '100%', sm: 280 }, zIndex: 100 }} - /> - } - /> - - {/* ============================================= || KPI Cards || ============================================= */} - - {KPI_META.map((item) => { - const Icon = item.icon; - return ( - - } - color={item.color} - loading={isLoadingReports} - /> - - ); - })} - - - {/* ============================================= || Filter Bar (date + search) || ============================================= */} - - - - - - - - - Orders · {datestatus} - - - {filteredRows.length} riders · {stats.orders} orders - - - - setOpen(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, - ml: 1, - 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')} - - - - - - - - - - {/* ============================================= || Table || ============================================= */} - - {isMobile ? ( - - {isLoadingReports && ( - - - Loading riders… - - - )} - {(!filteredRows || filteredRows.length === 0) && !isLoadingReports ? ( - - - - - - No riders to show - - - {searchword ? 'Try a different keyword.' : 'Pick a zone or date range to load the summary.'} - - - ) : ( - filteredRows.map((row, index) => { - const isOpen = openRow === row.userid; - const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); - const riderName = `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || '—'; - return ( - - - - - - - - {riderName} - - - #{String(index + 1).padStart(2, '0')} · ID #{row.userid} - - - - - - { - setSelectedRider({ - userid: row?.userid, - name: `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || `Rider ${row?.userid}` - }); - setLogDetails(null); - setMapOpen(true); - getuserdeliverylogs(row?.userid); - }} - sx={{ - bgcolor: soft('#0ea5e9'), - color: '#0ea5e9', - border: `1px solid ${edge('#0ea5e9')}`, - '&:hover': { bgcolor: '#0ea5e9', color: '#fff' } - }} - > - - - - - { - const isOpening = !isOpen; - setOpenRow(isOpening ? row.userid : null); - if (isOpening) fetchTenantSummary(row.userid); - }} - sx={{ - bgcolor: isOpen ? BRAND : soft(BRAND), - color: isOpen ? '#fff' : BRAND, - border: `1px solid ${edge(BRAND)}`, - '&:hover': { bgcolor: BRAND, color: '#fff' } - }} - > - {isOpen ? : } - - - - - } - > - - - } /> - - - } /> - - - } /> - - - } /> - - - } - label={`${Number(row.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - - - } - label={`${Number(row.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - } - label={formatNumberToRupees(amount).replace('₹', '').trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - - {/* per-tenant breakdown */} - {isOpen && ( - - - - - - - - Tenant Breakdown - - - }> - {loading && ( - - Loading… - - )} - {!loading && (!tenantData || tenantData.length === 0) ? ( - - No tenant breakdown available. - - ) : ( - tenantData?.map((sub, sidx) => ( - - - - - - - {sub.tenantname || '—'} - - - - - } /> - - - } /> - - - } /> - - - } /> - - - } - label={`${Number(sub.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - - - } - label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - } - label={formatNumberToRupees( - Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0) - ) - .replace('₹', '') - .trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - - )) - )} - - - - )} - - ); - }) - )} - - ) : ( - - - - - # - Rider - Orders - Pending - Cancelled - Delivered - KMS - Amount - Action - - - - - {isLoadingReports && } - {(!filteredRows || filteredRows.length === 0) && !isLoadingReports ? ( - - - - - - - - No riders to show - - - {searchword ? 'Try a different keyword.' : 'Pick a zone or date range to load the summary.'} - - - - - ) : ( - filteredRows.map((row, index) => { - const isOpen = openRow === row.userid; - const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); - return ( - - - - - {String(index + 1).padStart(2, '0')} - - - - - - - - - - - {`${row?.firstname || ''} ${row?.lastname || ''}`.trim() || '—'} - - - ID #{row.userid} - - - - - - - } /> - - - } /> - - - } /> - - - } /> - - - - - } - label={`${Number(row.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - } - label={`${Number(row.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - - - } - label={formatNumberToRupees(amount).replace('₹', '').trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - - - - { - setSelectedRider({ - userid: row?.userid, - name: `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || `Rider ${row?.userid}` - }); - setLogDetails(null); - setMapOpen(true); - getuserdeliverylogs(row?.userid); - }} - sx={{ - bgcolor: soft('#0ea5e9'), - color: '#0ea5e9', - border: `1px solid ${edge('#0ea5e9')}`, - '&:hover': { bgcolor: '#0ea5e9', color: '#fff' } - }} - > - - - - - { - const isOpening = !isOpen; - setOpenRow(isOpening ? row.userid : null); - if (isOpening) fetchTenantSummary(row.userid); - }} - sx={{ - bgcolor: isOpen ? BRAND : soft(BRAND), - color: isOpen ? '#fff' : BRAND, - border: `1px solid ${edge(BRAND)}`, - '&:hover': { bgcolor: BRAND, color: '#fff' } - }} - > - {isOpen ? : } - - - - - - - {/* ============================================= || Collapsible per-tenant breakdown || ============================================= */} - {isOpen && ( - - - - - - - - - - - Tenant Breakdown · {`${row?.firstname || ''} ${row?.lastname || ''}`.trim()} - - - -
- - - # - Client - All - Pending - Completed - Cancelled - KMS - Amount - - - - {loading && } - {!loading && (!tenantData || tenantData.length === 0) ? ( - - - - No tenant breakdown available. - - - - ) : ( - tenantData?.map((sub, sidx) => ( - - - - {String(sidx + 1).padStart(2, '0')} - - - - - - - - - {sub.tenantname || '—'} - - - - - } /> - - - } /> - - - } /> - - - } /> - - - - } - label={`${Number(sub.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - } - label={`${Number(sub.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - - - - } - label={formatNumberToRupees( - Math.max(Number(sub.charges) || 0, Number(sub.deliveryamt) || 0) - ) - .replace('₹', '') - .trim()} - tooltip="Total Amount" - minWidth={100} - /> - - - )) - )} - -
-
- - - - - )} - - ); - }) - )} - - - - )} - - {/* ============================================= || Total Bar || ============================================= */} - {filteredRows.length > 0 && ( - <> - - - - Grand Total - - - - - )} -
- - {/* ============================================= || Map Dialog || ============================================= */} - { - setMapOpen(false); - setLogDetails(null); - setSelectedRider(null); - }} - > - - { - setMapOpen(false); - setLogDetails(null); - setSelectedRider(null); - }} - /> - - - - {/* ============================================= || Date Filter Dialog || ============================================= */} - setOpen(false)} - onSelect={(range) => { - setStartdate(range.startDate); - setEnddate(range.endDate); - setDatestatus(range.label); - }} - /> - - ); -} diff --git a/src/routes/MainRoutes.js b/src/routes/MainRoutes.js index c0665e7..db59e63 100644 --- a/src/routes/MainRoutes.js +++ b/src/routes/MainRoutes.js @@ -22,9 +22,6 @@ const OrdersPreview = Loadable(lazy(() => import('pages/nearle/orders/OrdersPrev const Deliveries = Loadable(lazy(() => import('pages/nearle/deliveries/deliveries'))); const Customers = Loadable(lazy(() => import('pages/nearle/customers/customers'))); -const Invoice = Loadable(lazy(() => import('pages/nearle/invoice/invoice'))); -const InvoicePreview = Loadable(lazy(() => import('../pages/nearle/invoice/invoicePreview'))); - const Details = Loadable(lazy(() => import('pages/nearle/orders/details'))); const ViewProfile = Loadable(lazy(() => import('pages/nearle/viewProfile'))); @@ -36,11 +33,6 @@ const Createclient = Loadable(lazy(() => import('pages/nearle/clients/createclie const CreateCustomer = Loadable(lazy(() => import('pages/nearle/clients/createCustomer'))); const Requests = Loadable(lazy(() => import('pages/nearle/requests/requests'))); -const OrdersSummary = Loadable(lazy(() => import('pages/nearle/reports/ordersSummary'))); -const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails'))); -const RidersSummary = Loadable(lazy(() => import('pages/nearle/reports/ridersSummary'))); -const RidersLogs = Loadable(lazy(() => import('pages/nearle/reports/ridersLogs'))); -const Profitability = Loadable(lazy(() => import('pages/nearle/reports/profitability'))); const Riders = Loadable(lazy(() => import('pages/nearle/riders/riders'))); const Createrider = Loadable(lazy(() => import('pages/nearle/riders/createrider'))); @@ -85,24 +77,6 @@ const MainRoutes = { path: 'customers', element: }, - { - path: 'invoice', - children: [ - { - index: true, - element: - }, - { - path: 'preview', - element: - } - ] - }, - { - path: 'invoice/preview', - element: - }, - { path: 'requests', element: @@ -140,31 +114,6 @@ const MainRoutes = { path: 'customer/create', element: }, - { - path: 'reports', - children: [ - { - path: 'orderssummary', - element: - }, - { - path: 'ordersdetails', - element: - }, - { - path: 'riderssummary', - element: - }, - { - path: 'riderslogs', - element: - }, - { - path: 'profitability', - element: - } - ] - }, { path: 'dispatch', element: diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json index 7c7fe76..017c63e 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -11,13 +11,6 @@ "pricing": "Pricing", "customers": "Customers", "riders": "Milers", - "reports": "Reports", - "ordersummary": "Orders Summary", - "ordersdetails": "Orders Details", - "riderssummary": "Riders Summary", - "riderslogs": "Riders Logs", - "invoice": "Invoice", "dispatch": "Live Operations", - "profitability": "Profitability", "Doormile": "Doormile" }