diff --git a/.gitignore b/.gitignore
index 02724aa..db58a7c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,7 +79,7 @@ Thumbs.db
# Gatsby
.cache/
-public/
+
# VuePress
.vuepress/dist
diff --git a/src/components/third-party/ReactTable.js b/src/components/third-party/ReactTable.js
index da2980d..26d9eec 100644
--- a/src/components/third-party/ReactTable.js
+++ b/src/components/third-party/ReactTable.js
@@ -5,8 +5,10 @@ import React, { forwardRef, useEffect, useRef, useState } from 'react';
import { styled, useTheme } from '@mui/material/styles';
import {
Box,
+ Button,
Checkbox,
Chip,
+ CircularProgress,
FormControl,
Grid,
ListItemText,
@@ -493,20 +495,36 @@ SortingSelect.propTypes = {
// ==============================|| CSV EXPORT ||============================== //
-export const CSVExport = ({ data, filename, headers }) => {
+export const CSVExport = ({ data, filename, headers, label, style, btnLoading, onClick }) => {
return (
-
-
+
+ }
+ variant={btnLoading ? 'outlined' : 'contained'}
+ sx={{ ...style }}
+ disabled={btnLoading}
+ onClick={(e) => {
+ onClick?.(e);
+ }}
+ >
+ {btnLoading ? : label || 'Download'}
+
);
};
+export default CSVExport;
+
CSVExport.propTypes = {
data: PropTypes.array,
headers: PropTypes.any,
- filename: PropTypes.string
+ filename: PropTypes.string,
+ label: PropTypes.node,
+ style: PropTypes.object,
+ btnLoading: PropTypes.bool,
+ onClick: PropTypes.func
};
// ==============================|| EMPTY TABLE - NO DATA ||============================== //
diff --git a/src/config.js b/src/config.js
index 63af026..252fca5 100644
--- a/src/config.js
+++ b/src/config.js
@@ -5,7 +5,7 @@ export const facebookColor = '#3b5998';
export const linkedInColor = '#0e76a8';
// export const APP_DEFAULT_PATH = '/sample-page';
-export const APP_DEFAULT_PATH = '/nearle/orders';
+export const APP_DEFAULT_PATH = '/nearle/dispatch';
export const HORIZONTAL_MAX_ITEM = 6;
export const DRAWER_WIDTH = 260;
diff --git a/src/menu-items/nearle.js b/src/menu-items/nearle.js
index ed55be1..a92de17 100644
--- a/src/menu-items/nearle.js
+++ b/src/menu-items/nearle.js
@@ -5,6 +5,7 @@ import { AiOutlineDashboard } from 'react-icons/ai';
import { TbListDetails } from 'react-icons/tb';
import { LiaFileInvoiceSolid } from 'react-icons/lia';
import DirectionsBikeOutlinedIcon from '@mui/icons-material/DirectionsBikeOutlined';
+import RouteOutlinedIcon from '@mui/icons-material/RouteOutlined';
// assets
import {
@@ -49,6 +50,13 @@ const nearle = {
title: ,
type: 'group',
children: [
+ {
+ id: 'dispatch',
+ title: ,
+ type: 'item',
+ url: '/nearle/dispatch',
+ icon: RouteOutlinedIcon
+ },
{
id: 'orders',
title: ,
diff --git a/src/pages/nearle/api/api.js b/src/pages/nearle/api/api.js
index 65e0bc6..e3c31a2 100644
--- a/src/pages/nearle/api/api.js
+++ b/src/pages/nearle/api/api.js
@@ -184,10 +184,12 @@ export const gettenantlocations = async ({ queryKey }) => {
const [, searchLocation] = queryKey;
try {
const response = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations?tenantid=${tenid}&keyword=${searchLocation}`);
- return response.data?.details || []; // safe fallback
+ return response.data?.details || [];
} catch (error) {
+ // Must return an array — downstream consumers do `.map`/`.length` and a
+ // string here crashes the entire Locations page.
console.error('Error fetching tenant locations:', error);
- return error.message;
+ return [];
}
};
@@ -306,3 +308,126 @@ export const fetchRidersLogs = async ({ queryKey }) => {
console.log('fetchRidersLogs', riderLogsResponse.data.details);
return riderLogsResponse.data.details;
};
+
+// ==============================|| Dispatch / Preview APIs (ported from xpressconsole) ||============================== //
+
+// Returns the rider's latest periodic log entry — battery, GPS, status,
+// current order. Used by the Rider Info modal on the Dispatch page.
+export const getRiderPeriodicLogs = async (userid) => {
+ const url = `${process.env.REACT_APP_URL}/utils/getriderperiodiclogs${userid ? `?userid=${userid}` : ''}`;
+ const response = await axios.get(url);
+ if (response.data && response.data.status) return response.data.data;
+ return null;
+};
+
+// Fetches the riders dropdown list for an app location.
+export const fetchRidersList = async ({ queryKey }) => {
+ try {
+ const [, appId] = queryKey;
+ const { data } = await axios.get(`${process.env.REACT_APP_URL}/partners/getriders/?applocationid=${appId}`);
+ const response = data?.details
+ ? data.details.map((val) => ({
+ ...val,
+ label: `${val.firstname} ${val.lastname} | ${val.contactno}`
+ }))
+ : [];
+ return response;
+ } catch (err) {
+ OpenToast(err.message, 'error', 2000);
+ throw err;
+ }
+};
+
+// Optimise the orders (bike solver).
+export const createOptimisationDeliveries = async (deliveryData) => {
+ const response = await axios.post(`https://routes.workolik.com/api/v1/optimization/createdeliveries`, deliveryData.deliveries);
+ return response.data;
+};
+
+// Server-side step reconciliation after manual edits in Preview.
+export const reconcileSteps = async ({ riders }) => {
+ const response = await axios.post(`https://routes.workolik.com/api/v1/optimization/reconcile-steps`, { riders });
+ return response.data;
+};
+
+// Batch efficiency analysis — batch ∈ 'morning' | 'afternoon' | 'evening'.
+export const fetchBatchEfficiency = async ({ batch, tenantId }) => {
+ const response = await axios.post(
+ `https://routes.workolik.com/api/v1/batch/efficiency`,
+ { batch, tenant_id: tenantId },
+ {
+ headers: { 'Content-Type': 'application/json' },
+ validateStatus: () => true
+ }
+ );
+ return response.data;
+};
+
+// Final commit of dispatched deliveries — coerces userid/rider_id to int at
+// the boundary so a string upstream can't cause a 500 unmarshal error.
+export const finalCreatedeliveries = async (deliveryData) => {
+ const toInt = (v) => {
+ const n = Number(v);
+ return Number.isFinite(n) ? n : v;
+ };
+ const deliveries = (deliveryData.deliveries || []).map((d) => ({
+ ...d,
+ userid: toInt(d.userid),
+ rider_id: toInt(d.rider_id)
+ }));
+ const response = await axios.post(`https://jupiter.nearle.app/live/api/v1/deliveries/createdeliveries`, deliveries);
+ return response.data;
+};
+
+// Auto rider assignment via either the bike solver or the auto/multi-trip
+// solver. Body shape differs per mode; absent_riders is merged through.
+export const createAutomationDeliveries = async (variables) => {
+ const absentRiders = Array.isArray(variables.absent_riders) ? variables.absent_riders : [];
+
+ const url =
+ variables.selectedMode.value == 1
+ ? `https://routes.workolik.com/api/v1/optimization/riderassign?hypertuning_params=${variables.hypertuning_params}`
+ : `https://routemate.workolik.com/api/v1/optimization/riderassign?strategy=multi_trip`;
+
+ const body =
+ variables.selectedMode.value == 1
+ ? { deliveries: variables.deliveries, absent_riders: absentRiders }
+ : { ...(variables.data || {}), absent_riders: absentRiders };
+
+ const response = await axios.post(url, body);
+ return response.data;
+};
+
+// Push a notification to a rider after assignment.
+export const notifyRider = async (riderToken) => {
+ if (!riderToken) {
+ throw new Error('Invalid rider token');
+ }
+ const response = await axios.post(`${process.env.REACT_APP_URL}/utils/notifyuser`, {
+ token: riderToken,
+ notification: {
+ title: 'NearleXpress',
+ body: 'Orders have been placed for delivery. Kindly accept and process deliveries',
+ sound: 'ring',
+ image: ''
+ }
+ });
+ return response.data;
+};
+
+// Paginated deliveries fetch — supports both "All zones" (appId === 0) and
+// per-zone scoping. Returns { rows, nextPage } for useInfiniteQuery.
+export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => {
+ let [, appId, userid, currentStatus, startdate, enddate, rowsPerPage, searchword, tenantid, locationid, riderid] = queryKey;
+ currentStatus = currentStatus == 'All' ? 'all' : currentStatus;
+ const url =
+ appId === 0
+ ? `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?appuserid=${userid}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`
+ : `${process.env.REACT_APP_URL}/deliveries/getdeliveries/?applocationid=${appId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${searchword}&tenantid=${tenantid}&locationid=${locationid}&userid=${riderid}`;
+ const response = await axios.get(url);
+
+ return {
+ rows: response.data.details,
+ nextPage: response.data.details.length === Number(rowsPerPage) ? pageParam + 1 : undefined
+ };
+};
diff --git a/src/pages/nearle/dispatch/CompareDataPanel.js b/src/pages/nearle/dispatch/CompareDataPanel.js
new file mode 100644
index 0000000..f3162d7
--- /dev/null
+++ b/src/pages/nearle/dispatch/CompareDataPanel.js
@@ -0,0 +1,976 @@
+import React, { useMemo } from 'react';
+import {
+ MdPublic,
+ MdSwapHoriz,
+ MdExpandMore,
+ MdCheckCircle,
+ MdAccessTime,
+ MdStraighten,
+ MdErrorOutline,
+ MdFormatListBulleted,
+ MdTimer,
+ MdWarning,
+ MdClose,
+ MdSpeed,
+ MdStar,
+ MdFlag,
+ MdHourglassBottom
+} from 'react-icons/md';
+import {
+ stepColor,
+ getStatusStyle,
+ FINAL_STATUSES,
+ SKIPPED_STATUSES,
+ ordinal
+} from './dispatchShared';
+
+// Right-side data panel rendered in Compare mode. Pure presentation +
+// memoized derivations: feed it the comparison state from Dispatch and
+// the panel handles its own layout (compliance score, day overview, route
+// sequence with cascade grouping, KPIs, highlights, trips, focused-step
+// details, deviations, full step list).
+//
+// Props:
+// focusedRider — the rider whose day is being compared
+// compareDeltas — per-step actual deltas (see useMemo
+// in Dispatch.js)
+// compareSummary — day rollup: actualKm/onTime/etc
+// actualOrdered — compareDeltas sorted by sequenceStep (visit
+// order). Used by the route-sequence section.
+// focusedCompareStep — currently focused step (1..N) or null
+// setFocusedCompareStep — setter; pass a function-updater for toggle
+// sequenceOpen — whether the "Route sequence" section is open
+// setSequenceOpen — setter for sequenceOpen
+// expandedSeqGroups — Set of expanded sequence-diff group indices
+// setExpandedSeqGroups — setter (Set state)
+// onClose — called when the user clicks the × header btn
+function CompareDataPanel({
+ focusedRider,
+ compareDeltas,
+ compareSummary,
+ actualOrdered,
+ focusedCompareStep,
+ setFocusedCompareStep,
+ sequenceOpen,
+ setSequenceOpen,
+ expandedSeqGroups,
+ setExpandedSeqGroups,
+ onClose
+}) {
+ // All derivations live in a single useMemo so the cost of re-running
+ // them is paid only when an upstream input actually changes — not on
+ // every parent render (e.g. cursor moving over the map, sync toggle
+ // toggling, etc.). Keeping them grouped also makes the data contract
+ // visible at a glance.
+ const view = useMemo(() => {
+ const sum = compareSummary;
+ const totalSteps = sum.onTime + sum.late;
+ const deviations = compareDeltas.filter((d) => d.anomaly);
+ const delivered = compareDeltas.filter((d) =>
+ FINAL_STATUSES.has(String(d.orderstatus || '').toLowerCase())
+ ).length;
+ const skipped = compareDeltas.filter((d) =>
+ SKIPPED_STATUSES.has(String(d.orderstatus || '').toLowerCase())
+ ).length;
+ const stepDeltaPct =
+ sum.kmDeltaPct == null
+ ? ''
+ : sum.kmDeltaPct > 25
+ ? 'is-over'
+ : sum.kmDeltaPct < -5
+ ? 'is-under'
+ : '';
+
+ // Compliance score (0-100): 60% delivered + 25% on-time + 15% no-deviation.
+ const totalForScore = compareDeltas.length || 1;
+ const onTimeForScore = sum.onTime + sum.late || 1;
+ const score = Math.round(
+ (delivered / totalForScore) * 60 +
+ (sum.onTime / onTimeForScore) * 25 +
+ ((totalForScore - sum.anomalies) / totalForScore) * 15
+ );
+ const scoreColor = score >= 85 ? '#16a34a' : score >= 65 ? '#f59e0b' : '#dc2626';
+ const scoreLabel = score >= 85 ? 'Excellent' : score >= 65 ? 'Acceptable' : 'Needs review';
+
+ // KPIs derived from delivery timestamps.
+ const withActual = compareDeltas.filter((d) => d.actualTs);
+ const firstDelivery = withActual.reduce(
+ (acc, d) => (!acc || d.actualTs.isBefore(acc) ? d.actualTs : acc),
+ null
+ );
+ const lastDelivery = withActual.reduce(
+ (acc, d) => (!acc || d.actualTs.isAfter(acc) ? d.actualTs : acc),
+ null
+ );
+ const activeMin =
+ firstDelivery && lastDelivery
+ ? Math.max(0, lastDelivery.diff(firstDelivery, 'minute'))
+ : 0;
+ const avgPerStop =
+ compareDeltas.length > 1
+ ? Math.round(activeMin / (compareDeltas.length - 1))
+ : 0;
+ const avgSpeed =
+ activeMin > 0 ? (sum.actualKm / (activeMin / 60)).toFixed(1) : null;
+
+ // Best / worst step.
+ const readyDeltas = compareDeltas.filter(
+ (d) => !d.isLoading && d.coordsCount > 0
+ );
+ const bestStep =
+ readyDeltas
+ .filter((d) => d.timeDeltaMin != null && !d.anomaly)
+ .sort((a, b) => a.timeDeltaMin - b.timeDeltaMin)[0] || null;
+ const worstStep =
+ readyDeltas
+ .filter((d) => d.anomaly)
+ .sort((a, b) => {
+ const sa =
+ Math.abs(a.kmDeltaPct || 0) + (a.timeDeltaMin > 0 ? a.timeDeltaMin : 0);
+ const sb =
+ Math.abs(b.kmDeltaPct || 0) + (b.timeDeltaMin > 0 ? b.timeDeltaMin : 0);
+ return sb - sa;
+ })[0] || null;
+
+ // Route sequence — which actual-visit positions don't match the
+ // dispatch-planned step.
+ const outOfOrderSteps = actualOrdered.filter((d, i) => {
+ const planned = d.order?.step;
+ return planned != null && planned !== i + 1;
+ });
+
+ // Cascade-aware grouping of out-of-order steps: consecutive entries
+ // with the same `delta` collapse into one "N consecutive shifted +K"
+ // card so a single bad first stop doesn't paint 12 noisy rows.
+ const seqRuns = [];
+ outOfOrderSteps.forEach((d) => {
+ const planned = d.order?.step;
+ const actualPos =
+ actualOrdered.findIndex((x) => x.sequenceStep === d.sequenceStep) + 1;
+ const delta = actualPos - planned;
+ const last = seqRuns[seqRuns.length - 1];
+ if (last && last.delta === delta && last.lastActualPos + 1 === actualPos) {
+ last.items.push({ d, planned, actualPos, delta });
+ last.lastActualPos = actualPos;
+ } else {
+ seqRuns.push({
+ delta,
+ items: [{ d, planned, actualPos, delta }],
+ lastActualPos: actualPos
+ });
+ }
+ });
+
+ // Trip-by-trip rollup.
+ const tripBuckets = {};
+ focusedRider.orders.forEach((o) => {
+ const t = o.trip_number || 1;
+ if (!tripBuckets[t]) tripBuckets[t] = [];
+ tripBuckets[t].push(o);
+ });
+ const tripList = Object.entries(tripBuckets)
+ .sort(([a], [b]) => Number(a) - Number(b))
+ .map(([tNum, tOrders]) => ({
+ tNum,
+ count: tOrders.length,
+ actualKm: tOrders.reduce(
+ (s, o) => s + parseFloat(o.actualkms || o.kms || 0),
+ 0
+ ),
+ delivered: tOrders.filter((o) =>
+ FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase())
+ ).length
+ }));
+
+ return {
+ sum,
+ totalSteps,
+ deviations,
+ delivered,
+ skipped,
+ stepDeltaPct,
+ score,
+ scoreColor,
+ scoreLabel,
+ firstDelivery,
+ lastDelivery,
+ activeMin,
+ avgPerStop,
+ avgSpeed,
+ bestStep,
+ worstStep,
+ outOfOrderSteps,
+ seqRuns,
+ tripList
+ };
+ }, [focusedRider, compareDeltas, compareSummary, actualOrdered]);
+
+ const focused =
+ focusedCompareStep != null
+ ? compareDeltas.find((d) => d.sequenceStep === focusedCompareStep)
+ : null;
+
+ const toggleSeqGroup = (idx) => {
+ setExpandedSeqGroups((prev) => {
+ const next = new Set(prev);
+ if (next.has(idx)) next.delete(idx);
+ else next.add(idx);
+ return next;
+ });
+ };
+
+ const focusStep = (sequenceStep) => {
+ setFocusedCompareStep((prev) => (prev === sequenceStep ? null : sequenceStep));
+ };
+
+ // Renders a single shifted-step diff card (used both stand-alone and
+ // nested under an expanded group).
+ const renderDiffRow = (item, focusable = true) => {
+ const { d, planned, actualPos, delta } = item;
+ return (
+
focusStep(d.sequenceStep)}
+ >
+
+ {planned || d.sequenceStep}
+
+
+
+ {d.deliverycustomer || `Step ${planned || d.sequenceStep}`}
+
+
+ Visited {ordinal(actualPos)}{' '}
+ · planned {ordinal(planned)}
+
+
+
+ {delta > 0 ? `+${delta}` : `${delta}`}
+
+
+ );
+ };
+
+ const {
+ sum,
+ totalSteps,
+ deviations,
+ delivered,
+ skipped,
+ stepDeltaPct,
+ score,
+ scoreColor,
+ scoreLabel,
+ firstDelivery,
+ lastDelivery,
+ activeMin,
+ avgPerStop,
+ avgSpeed,
+ bestStep,
+ worstStep,
+ outOfOrderSteps,
+ seqRuns,
+ tripList
+ } = view;
+
+ return (
+
+ );
+}
+
+export default CompareDataPanel;
diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css
new file mode 100644
index 0000000..76efa51
--- /dev/null
+++ b/src/pages/nearle/dispatch/Dispatch.css
@@ -0,0 +1,9848 @@
+:root {
+ --bg: #ffffff;
+ --bg-sub: #f8fafc;
+ --bg-card: #ffffff;
+ --border: #e2e8f0;
+ --border-active: #3b82f6;
+ --text: #1e293b;
+ --text-muted: #64748b;
+ --accent: #3b82f6;
+ --accent-soft: rgba(59, 130, 246, 0.08);
+ --kitchen: #f59e0b;
+ --kitchen-soft: rgba(245, 158, 11, 0.1);
+ --success: #22c55e;
+ --shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+ --shadow-lg: 0 10px 25px -5px rgba(0, 0, 0, 0.08);
+}
+
+.dispatch-container {
+ width: calc(100% + 48px);
+ height: calc(100vh - 88px);
+ margin: -24px;
+ display: flex;
+ flex-direction: column;
+ background: var(--bg);
+ color: var(--text);
+ font-family: 'Inter', -apple-system, sans-serif;
+ overflow: hidden;
+ position: relative;
+}
+
+/* Embedded mode: rendered inside a parent container (e.g. a Dialog),
+ so drop the negative margin and viewport-based sizing that assumes
+ the standalone /dispatch page is wrapped in MainCard's 24px padding. */
+.dispatch-container.embedded {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ flex: 1;
+ min-height: 0;
+}
+
+.dispatch-container * {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+/* Header */
+.dispatch-container #hdr {
+ height: 56px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 24px;
+ background: var(--bg);
+ border-bottom: 1px solid var(--border);
+ z-index: 1010;
+}
+
+.dispatch-container .logo {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.dispatch-container .logo-badge {
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ background: linear-gradient(135deg, #3b82f6, #2563eb);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 800;
+ font-size: 14px;
+ color: #fff;
+}
+
+.dispatch-container .logo-name {
+ font-size: 18px;
+ font-weight: 800;
+ color: var(--text);
+ letter-spacing: -0.02em;
+}
+
+.dispatch-container .logo-name em {
+ color: var(--accent);
+ font-style: normal;
+ opacity: 0.8;
+}
+
+/* Operating-city pill — sits to the RIGHT of the "Dispatch" heading inline. */
+/* The location pill is now an interactive dropdown trigger. Wrapped in
+ .logo-city-wrap so the absolute-positioned menu below anchors to it. */
+.dispatch-container .logo-city-wrap {
+ position: relative;
+ display: inline-block;
+}
+
+.dispatch-container .logo-city {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 8px 4px 10px;
+ border-radius: 999px;
+ background: rgba(123, 31, 162, 0.08);
+ border: 1px solid rgba(123, 31, 162, 0.25);
+ color: #7b1fa2;
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.02em;
+ line-height: 1;
+ cursor: pointer;
+ font-family: inherit;
+ transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
+}
+
+.dispatch-container .logo-city:hover {
+ background: rgba(123, 31, 162, 0.14);
+ border-color: rgba(123, 31, 162, 0.45);
+}
+
+.dispatch-container .logo-city.open {
+ background: rgba(123, 31, 162, 0.18);
+ border-color: rgba(123, 31, 162, 0.55);
+ box-shadow: 0 4px 12px rgba(123, 31, 162, 0.18);
+}
+
+.dispatch-container .logo-city svg {
+ font-size: 13px;
+ flex-shrink: 0;
+}
+
+.dispatch-container .logo-city-caret {
+ font-size: 15px;
+ transition: transform 0.2s ease;
+}
+
+.dispatch-container .logo-city.open .logo-city-caret {
+ transform: rotate(180deg);
+}
+
+.dispatch-container .logo-city-text {
+ max-width: 180px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* Dropdown menu — anchored under the trigger, scrolls if there are many hubs. */
+.dispatch-container .logo-city-menu {
+ position: absolute;
+ top: calc(100% + 6px);
+ left: 0;
+ min-width: 200px;
+ max-height: 320px;
+ overflow-y: auto;
+ background: #fff;
+ border: 1px solid rgba(123, 31, 162, 0.18);
+ border-radius: 12px;
+ box-shadow: 0 16px 36px rgba(15, 23, 42, 0.16);
+ padding: 6px;
+ z-index: 1000;
+ animation: logo-city-menu-in 0.14s ease-out;
+}
+
+@keyframes logo-city-menu-in {
+ from {
+ opacity: 0;
+ transform: translateY(-4px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.dispatch-container .logo-city-menu::-webkit-scrollbar {
+ width: 6px;
+}
+
+.dispatch-container .logo-city-menu::-webkit-scrollbar-thumb {
+ background: rgba(123, 31, 162, 0.3);
+ border-radius: 999px;
+}
+
+.dispatch-container .logo-city-option {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ padding: 8px 10px;
+ border: 0;
+ background: transparent;
+ border-radius: 8px;
+ font-size: 12px;
+ font-weight: 600;
+ color: #1e293b;
+ cursor: pointer;
+ font-family: inherit;
+ text-align: left;
+ transition: background 0.12s ease;
+}
+
+.dispatch-container .logo-city-option:hover {
+ background: rgba(123, 31, 162, 0.06);
+}
+
+.dispatch-container .logo-city-option.active {
+ background: rgba(123, 31, 162, 0.1);
+ color: #7b1fa2;
+}
+
+.dispatch-container .logo-city-option-icon {
+ font-size: 14px;
+ color: #7b1fa2;
+ flex-shrink: 0;
+}
+
+.dispatch-container .logo-city-option span:not(.logo-city-option-check) {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.dispatch-container .logo-city-option-check {
+ color: #7b1fa2;
+ font-weight: 800;
+ flex-shrink: 0;
+}
+
+.dispatch-container .hdr-sep {
+ width: 1px;
+ height: 20px;
+ background: var(--border);
+ margin: 0 4px;
+}
+
+.dispatch-container .hdr-meta {
+ font-size: 12px;
+ color: var(--text-muted);
+ font-weight: 500;
+}
+
+.dispatch-container #clock {
+ font-size: 13px;
+ color: var(--text);
+ font-weight: 600;
+ font-family: 'JetBrains Mono', monospace;
+ background: var(--bg-sub);
+ padding: 7px 16px;
+ border-radius: 10px;
+ border: 1px solid var(--border);
+}
+
+/* Header right-cluster — profit/loss + orders pill + date picker, sits to the
+ LEFT of the running clock. Pushed against the clock with margin-left:auto so
+ the .logo on the left stays anchored and the cluster floats right. */
+.dispatch-container .hdr-stats {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ margin-left: auto;
+ margin-right: 16px;
+ min-width: 0;
+ flex-wrap: nowrap;
+}
+
+/* Tabs */
+.dispatch-container #strat-row {
+ height: 54px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 0 24px;
+ background: var(--bg);
+ border-bottom: 1px solid var(--border);
+}
+
+.dispatch-container .sbt {
+ padding: 8px 14px;
+ border-radius: 10px;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ background: var(--bg);
+ color: var(--text-muted);
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ line-height: 1;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ font-family: inherit;
+}
+
+.dispatch-container .sbt:hover {
+ background: var(--bg-sub);
+ color: var(--text);
+ border-color: var(--text-muted);
+}
+
+.dispatch-container .sbt.active {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: #fff;
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.25);
+}
+
+/* SVG icon slot inside each tab button — fixed square, color inherits from button
+ so active-state white propagates without per-tab overrides. */
+.dispatch-container .sbt .sbt-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ font-size: 18px;
+ line-height: 1;
+ flex-shrink: 0;
+ color: inherit;
+}
+
+.dispatch-container .sbt .sbt-icon svg {
+ width: 1em;
+ height: 1em;
+ display: block;
+ /* react-icons SVGs fill with currentColor by default — this just ensures
+ consistent baseline alignment with the label next to them. */
+ vertical-align: middle;
+}
+
+/* Strat-row quick stats — total orders + profit/loss chips next to the view-mode buttons */
+.dispatch-container .strat-stats {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ margin-left: 8px;
+ padding-left: 12px;
+ border-left: 1px solid var(--border);
+ height: 32px;
+}
+
+/* Right-floating variant — used for the profit/loss chip when there's no
+ live-controls block to nest inside. */
+.dispatch-container .strat-stats.strat-stats-right {
+ margin-left: auto;
+ padding-left: 0;
+ border-left: none;
+}
+
+.dispatch-container .strat-stat {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 7px 15px;
+ border-radius: 999px;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 1;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--text);
+ transition: all 0.15s ease;
+ white-space: nowrap;
+}
+
+.dispatch-container .strat-stat-icon {
+ font-size: 13px;
+ line-height: 1;
+}
+
+.dispatch-container .strat-stat-label {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--text-muted);
+}
+
+.dispatch-container .strat-stat-value {
+ font-size: 13px;
+ font-weight: 800;
+}
+
+.dispatch-container .strat-stat-orders {
+ background: var(--accent-soft);
+ border-color: rgba(59, 130, 246, 0.25);
+}
+
+.dispatch-container .strat-stat-orders .strat-stat-value {
+ color: var(--accent);
+}
+
+.dispatch-container .strat-stat-profit {
+ background: rgba(34, 197, 94, 0.1);
+ border-color: rgba(34, 197, 94, 0.3);
+}
+
+.dispatch-container .strat-stat-profit .strat-stat-value,
+.dispatch-container .strat-stat-profit .strat-stat-label {
+ color: var(--success);
+}
+
+.dispatch-container .strat-stat-loss {
+ background: rgba(239, 68, 68, 0.1);
+ border-color: rgba(239, 68, 68, 0.35);
+}
+
+.dispatch-container .strat-stat-loss .strat-stat-value,
+.dispatch-container .strat-stat-loss .strat-stat-label {
+ color: #dc2626;
+}
+
+/* Live data controls (date picker + load status) */
+.dispatch-container .live-controls {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.dispatch-container .live-status {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-muted);
+ padding: 7px 15px;
+ border-radius: 999px;
+ background: var(--bg-sub);
+ border: 1px solid var(--border);
+}
+
+.dispatch-container .live-status-ready {
+ color: var(--success);
+}
+
+.dispatch-container .live-status-error {
+ color: #ef4444;
+}
+
+.dispatch-container .live-status-sub {
+ color: var(--text-muted);
+ font-weight: 500;
+ font-size: 11px;
+ opacity: 0.85;
+}
+
+.dispatch-container .live-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--accent);
+ animation: live-pulse 1.2s ease-in-out infinite;
+}
+
+.dispatch-container .live-dot.ready {
+ background: var(--success);
+ animation: none;
+}
+
+.dispatch-container .live-dot.error {
+ background: #ef4444;
+ animation: none;
+}
+
+@keyframes live-pulse {
+
+ 0%,
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+
+ 50% {
+ opacity: 0.4;
+ transform: scale(0.85);
+ }
+}
+
+/* ── Date picker chip ─────────────────────────────────────────────
+ Three-part pill: prev-day arrow ◂ | formatted-date card | ▸ next-day
+ arrow. The center card overlays a transparent native
+ so clicking anywhere on the chip opens the OS date dialog while still
+ showing a glanceable formatted value (`Mon, May 25, 2026`). A small
+ "Today" badge appears when the picked date matches today, and the
+ next-day arrow disables itself there.
+
+ Design language: matches the Compare-button family — soft white card,
+ indigo border + halo on hover/focus, subtle lift on interaction.
+ ──────────────────────────────────────────────────────────────── */
+.dispatch-container .date-chip {
+ position: relative;
+ /* anchors .date-cal-popover */
+ display: inline-flex;
+ align-items: stretch;
+ gap: 0;
+ background: #ffffff;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+ border-radius: 12px;
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04),
+ 0 4px 12px rgba(15, 23, 42, 0.06);
+ transition: border-color 0.18s ease, box-shadow 0.18s ease,
+ transform 0.18s ease;
+}
+
+.dispatch-container .date-chip.is-open {
+ border-color: #6366f1;
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2),
+ 0 12px 30px rgba(99, 102, 241, 0.22);
+}
+
+.dispatch-container .date-chip:hover {
+ border-color: rgba(99, 102, 241, 0.45);
+ box-shadow: 0 2px 4px rgba(15, 23, 42, 0.06),
+ 0 8px 22px rgba(99, 102, 241, 0.15);
+ transform: translateY(-1px);
+}
+
+.dispatch-container .date-chip:focus-within {
+ border-color: #6366f1;
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2),
+ 0 8px 22px rgba(99, 102, 241, 0.22);
+}
+
+/* Center card — visible chrome the operator reads. Renders as a
+
+
+
+
+
+
+ setTabValue(v)} sx={{ minHeight: 40 }}>
+
+
+
+
+
+
+ {tabValue === 0 && dispatchPreviewData && (
+ openChangeRider(focusedRider, order)}
+ />
+ )}
+ {tabValue === 1 && (
+
+ {reconcileRiders.length === 0 ? (
+
+ No rider data available to reconcile.
+
+ ) : (
+
+
+ {hasReconciled
+ ? 'Steps have been reconciled. The Dispatch tab and Assign payload are updated.'
+ : 'Click a numbered step to change its rider. Hit Reconcile to verify the corrected steps with the server.'}
+
+
+ {reconcileRiders.map((r) => {
+ const totalKms = r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0);
+ return (
+
+
+
+
+
+
+
+
+ {r.rider_name}
+
+
+ ID: {r.rider_id}
+
+
+
+
+
+
+
+
+
+
+ {r.orders.map((o, idx) => {
+ const stepNum = o.step ?? idx + 1;
+ const color = stepColor(Number(stepNum) - 1);
+ return (
+
+ Order #{o.orderid}
+ {o.deliveryaddress || o.deliverysuburb || ''}
+ Click to change rider
+
+ }
+ >
+ openChangeRider(r, o)}
+ sx={{
+ width: 36,
+ height: 36,
+ borderRadius: '50%',
+ bgcolor: color,
+ color: '#fff',
+ display: 'inline-flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ fontWeight: 800,
+ fontSize: 14,
+ cursor: 'pointer',
+ boxShadow:
+ '0 0 0 2px rgba(255,255,255,0.6), 0 1px 3px rgba(15,23,42,0.15)',
+ transition: 'transform 0.15s',
+ '&:hover': { transform: 'scale(1.08)' }
+ }}
+ >
+ {stepNum}
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+
+ }
+ onClick={handleReconcile}
+ disabled={reconcileLoading || dirtyRiderIds.size === 0}
+ sx={{ minWidth: 220, borderRadius: '10px', textTransform: 'none', fontWeight: 700 }}
+ >
+ {reconcileLoading
+ ? 'Reconciling...'
+ : dirtyRiderIds.size === 0
+ ? 'Reconcile'
+ : `Reconcile (${dirtyRiderIds.size})`}
+
+
+
+ )}
+
+ )}
+
+
+
+
+ }
+ onClick={() => navigate(-1)}
+ >
+ Back
+
+
+ Assign Orders
+
+
+
+
+
+
+ );
+};
+
+// Mirrors the orders.js deliveryamt recalc — applied at render-time so the
+// Assign payload always reflects the current cache without a useEffect.
+function computeDeliveryAmounts(list) {
+ return list.map((item) => {
+ const cumulativeKms = Number(item.cumulativekms || 0);
+ const minKm = Number(item.minkm || 0);
+ const basePrice = Number(item.baseprice || 0);
+ const pricePerKm = Number(item.priceperkm || 0);
+ if (cumulativeKms <= minKm) return { ...item, deliveryamt: basePrice };
+ return { ...item, deliveryamt: (cumulativeKms - minKm) * pricePerKm + basePrice };
+ });
+}
+
+export default Preview;
diff --git a/src/pages/nearle/dispatch/dispatchShared.js b/src/pages/nearle/dispatch/dispatchShared.js
new file mode 100644
index 0000000..dfdc118
--- /dev/null
+++ b/src/pages/nearle/dispatch/dispatchShared.js
@@ -0,0 +1,65 @@
+// Shared constants and pure helpers for the Dispatch page and its
+// extracted sub-components (CompareDataPanel, etc.). Lives outside
+// Dispatch.js so we don't create a circular import between the host
+// component and the child views.
+
+// Status palette — single source of truth for the status pill colors
+// rendered on rider cards, order rows, step lists, and tooltips.
+export const STATUS_STYLES = {
+ created: { label: 'Created', bg: '#3b82f6', fg: '#fff' },
+ pending: { label: 'Pending', bg: '#f59e0b', fg: '#fff' },
+ accepted: { label: 'Accepted', bg: '#8b5cf6', fg: '#fff' },
+ arrived: { label: 'Arrived', bg: '#ea580c', fg: '#fff' },
+ picked: { label: 'Picked', bg: '#0ea5e9', fg: '#fff' },
+ active: { label: 'Active', bg: '#0ea5e9', fg: '#fff' },
+ delivered: { label: 'Delivered', bg: '#22c55e', fg: '#fff' },
+ skipped: { label: 'Skipped', bg: '#94a3b8', fg: '#fff' },
+ cancelled: { label: 'Cancelled', bg: '#ef4444', fg: '#fff' }
+};
+
+export const getStatusStyle = (status) =>
+ STATUS_STYLES[String(status || '').toLowerCase()] || {
+ label: status || 'Unknown',
+ bg: '#64748b',
+ fg: '#fff'
+ };
+
+// Order-status sets used for completion / skipped decisions across the
+// rider list, the planned-route renderer, and the compare data panel.
+export const FINAL_STATUSES = new Set(['delivered']);
+export const SKIPPED_STATUSES = new Set(['cancelled', 'skipped']);
+
+// Per-step palette — wider and more deliberately spaced than the rider
+// palette so a 10-stop day reads as 10 distinct colors on the compare
+// map's polylines + pins.
+export const STEP_PALETTE = [
+ '#2563eb', // blue-600
+ '#dc2626', // red-600
+ '#16a34a', // green-600
+ '#ea580c', // orange-600
+ '#9333ea', // purple-600
+ '#0891b2', // cyan-600
+ '#ca8a04', // yellow-600
+ '#db2777', // pink-600
+ '#0f766e', // teal-700
+ '#7c3aed', // violet-600
+ '#65a30d', // lime-600
+ '#0284c7', // sky-600
+ '#b91c1c', // red-700
+ '#15803d', // green-700
+ '#a16207', // yellow-700
+ '#86198f' // fuchsia-800
+];
+
+export const stepColor = (i) =>
+ STEP_PALETTE[((i % STEP_PALETTE.length) + STEP_PALETTE.length) % STEP_PALETTE.length];
+
+// Pure helper — converts 1, 2, 3, 21 → "1st", "2nd", "3rd", "21st". Used
+// by the compare data panel for the route-sequence diff list ("Visited
+// 4th · planned 2nd").
+export const ordinal = (n) => {
+ if (n == null) return '';
+ const s = ['th', 'st', 'nd', 'rd'];
+ const v = n % 100;
+ return n + (s[(v - 20) % 10] || s[v] || s[0]);
+};
diff --git a/src/pages/nearle/invoice/invoice.js b/src/pages/nearle/invoice/invoice.js
index e9908fc..86df296 100644
--- a/src/pages/nearle/invoice/invoice.js
+++ b/src/pages/nearle/invoice/invoice.js
@@ -1,11 +1,12 @@
import React, { useEffect, useState, useRef } from 'react';
-import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
-import { Empty } from 'antd';
import {
+ Avatar,
+ Box,
Grid,
- Button,
- Divider,
+ IconButton,
+ Paper,
+ Stack,
Table,
TableBody,
TableCell,
@@ -13,151 +14,117 @@ import {
TableHead,
TablePagination,
TableRow,
- Tabs,
- Tab,
- Typography,
- Box,
- OutlinedInput,
- InputAdornment,
- IconButton,
Tooltip,
- Dialog,
- DialogTitle,
- DialogContent,
- Stack,
- Chip
+ Typography,
+ Skeleton,
+ InputBase
} from '@mui/material';
import axios from 'axios';
import dayjs from 'dayjs';
import { useNavigate } from 'react-router-dom';
-import VisibilityIcon from '@mui/icons-material/Visibility';
-import { useTheme } from '@mui/material/styles';
-import { DateRangePicker } from 'mui-daterange-picker';
-import { addDays, addMonths, addWeeks, addYears, endOfMonth, endOfWeek, endOfYear, startOfMonth, startOfWeek, startOfYear } from 'date-fns';
+import {
+ MdReceiptLong,
+ MdAccessTime,
+ MdCheckCircle,
+ MdHourglassEmpty,
+ MdWarning,
+ MdSearch,
+ MdClear,
+ MdVisibility,
+ MdInventory2,
+ MdCalendarMonth,
+ MdEventBusy
+} from 'react-icons/md';
import Loader from 'components/Loader';
-import { DownloadOutlined, PrinterFilled } from '@ant-design/icons';
-import ReactToPrint from 'react-to-print';
-// import nearleLogo from '../../assets/images/nearleLogo.png';
-import { DashboardOutlined } from '@ant-design/icons';
-import { HiHandThumbDown, HiHandThumbUp } from 'react-icons/hi2';
-import LoadingIcons from 'react-loading-icons';
-import AddIcon from '@mui/icons-material/Add';
-import MainCard from 'components/MainCard';
-import { SearchOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
-import ClearIcon from '@mui/icons-material/Clear';
+// ============================================================================
+// Design tokens — shared with the rest of the redesigned operator pages.
+// ============================================================================
+const DT = {
+ radiusPill: 999,
+ radiusCard: 16,
+ shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
+ shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
+ shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
+ textPrimary: '#0f172a',
+ textSecondary: '#64748b',
+ textMuted: '#94a3b8',
+ borderSubtle: '#e2e8f0',
+ divider: '#f1f5f9',
+ surface: '#ffffff',
+ surfaceAlt: '#f8fafc'
+};
+const dtA = (c, suffix) => `${c}${suffix}`;
+const tint = (c) => dtA(c, '08');
+const soft = (c) => dtA(c, '18');
+const ring = (c) => dtA(c, '26');
+const edge = (c) => dtA(c, '55');
-const columns = [
- { id: 'sno', label: 'sno' },
- { id: 'client', label: 'client' },
- { id: 'invoice ', label: 'Invoice Id ' },
+const BRAND = '#662582';
+const BRAND_LIGHT = '#9255AB';
- {
- id: 'invoice date',
- label: 'invoice date',
- align: 'left'
- // format: (value) => value.toLocaleString("en-US"),
- },
- {
- id: 'due date',
- label: 'due date',
- align: 'left'
- // format: (value) => value.toLocaleString("en-US"),
- },
- {
- id: 'itemcount',
- label: 'Count',
- align: 'left'
- // format: (value) => typeof value === "number" && value.toFixed(2),
- },
-
- { id: 'amount', label: 'amount', align: 'right' },
- { id: 'action', label: 'action', align: 'center' }
+// Status palette for the four invoice buckets.
+const INVOICE_STATUS_TABS = [
+ { idx: 0, billStatus: 0, label: 'All', color: BRAND, icon: MdReceiptLong, countKey: 'totalcount' },
+ { idx: 1, billStatus: 1, label: 'Open', color: '#f59e0b', icon: MdHourglassEmpty, countKey: 'pendingcount' },
+ { idx: 2, billStatus: 2, label: 'Overdue', color: '#ef4444', icon: MdEventBusy, countKey: 'overduecount' },
+ { idx: 3, billStatus: 3, label: 'Paid', color: '#10b981', icon: MdCheckCircle, countKey: 'paidcount' }
];
-function CustomTabPanel(props) {
- const { children, value, index, ...other } = props;
- return (
-
- {value === index && (
-
- {children}
-
- )}
-
- );
-}
-function a11yProps(index) {
- return {
- id: `simple-tab-${index}`,
- 'aria-controls': `simple-tabpanel-${index}`
- };
-}
function formatNumberToRupees(value) {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2
- }).format(value);
+ }).format(value || 0);
}
+
const Invoice = () => {
- const theme = useTheme();
const navigate = useNavigate();
- const [page, setPage] = React.useState(0);
- const [rowsPerPage, setRowsPerPage] = React.useState(10);
- const [value, setValue] = React.useState(0);
- const [content, setcontent] = React.useState('one');
- const [opendate, setOpendate] = useState(false);
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(10);
+ const [value, setValue] = useState(0);
const [deliveryList, setDeliveryList] = useState([]);
- const [predialog, setpredialog] = useState(false);
- const [selected, setselected] = useState({});
- const componentRef = useRef(null);
const [billStatus, setBillStatus] = useState(0);
const [isloader, setIsLoader] = useState(false);
- const [insightdata, setinsightdata] = useState(false);
+ const [insightdata, setInsightdata] = useState({});
const [search, setSearch] = useState('');
+ const [debouncedSearch, setDebouncedSearch] = useState('');
const tenid = localStorage.getItem('tenantid');
const textFieldRef = useRef(null);
- /* ============================================= || handleKeyPress (ctrl+k)| ============================================= */
+ // ============================================= || handleKeyPress (ctrl+k) || =============================================
useEffect(() => {
const handleKeyPress = (event) => {
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
-
- textFieldRef.current.focus();
+ textFieldRef.current && textFieldRef.current.focus();
}
if (event.key === 'Escape' && document.activeElement === textFieldRef.current) {
- // Remove focus from the TextField
textFieldRef.current.blur();
}
};
document.addEventListener('keydown', handleKeyPress);
-
- return () => {
- document.removeEventListener('keydown', handleKeyPress);
- };
+ return () => document.removeEventListener('keydown', handleKeyPress);
}, []);
- const handleChangePage = (event, newPage) => {
- setPage(newPage);
- };
+ // Debounce search.
+ useEffect(() => {
+ const t = setTimeout(() => setDebouncedSearch(search), 350);
+ return () => clearTimeout(t);
+ }, [search]);
+ const handleChangePage = (event, newPage) => setPage(newPage);
const handleChangeRowsPerPage = (event) => {
- setRowsPerPage(+event?.target?.value);
+ setRowsPerPage(+event.target.value);
setPage(0);
};
- const handleChange = (event, newValue) => {
- setValue(newValue);
- };
-
- /* ============================================= || fetchinvoiceinsight| ============================================= */
+ // ============================================= || fetchinvoiceinsight || =============================================
const fetchinvoiceinsight = async () => {
try {
const insightResponse = await axios.get(`${process.env.REACT_APP_URL}/invoice/getinvoiceinsight/?tenantid=${tenid}`);
- console.log('insightResponse', insightResponse.data.details);
- setinsightdata(insightResponse.data.details);
+ setInsightdata(insightResponse.data.details || {});
} catch (error) {
console.log('insightResponse', error);
}
@@ -166,374 +133,574 @@ const Invoice = () => {
fetchinvoiceinsight();
}, []);
- /* ============================================= || fetchdeliverylist| ============================================= */
+ // ============================================= || fetchdeliverylist || =============================================
const fetchdeliverylist = async () => {
setIsLoader(true);
- console.log('billstatus', billStatus);
try {
- let url = `${process.env.REACT_APP_URL}/invoice/getallinvoice/?billstatus=${billStatus}&tenantid=${tenid}`;
+ const url = `${process.env.REACT_APP_URL}/invoice/getallinvoice/?billstatus=${billStatus}&tenantid=${tenid}`;
const deliveyResponse = await axios.get(url);
- console.log('fetchdeliverylist', deliveyResponse.data.details);
- setDeliveryList(deliveyResponse.data.details);
- setIsLoader(false);
+ setDeliveryList(deliveyResponse.data.details || []);
} catch (error) {
console.log('fetchdeliverylist', error);
+ } finally {
+ setIsLoader(false);
}
};
useEffect(() => {
fetchdeliverylist();
+ setPage(0);
}, [billStatus]);
- useEffect(() => {
- console.log('selected', selected);
- }, [selected]);
- const filteredList = () => {
- let filterdata = deliveryList.filter((data) => data.invoiceno.toLowerCase().includes(search.toLowerCase()));
- setDeliveryList(filterdata);
+ // Client-side search filter — case-insensitive on invoice number.
+ const filteredList = (deliveryList || []).filter((d) =>
+ String(d.invoiceno || '').toLowerCase().includes(String(debouncedSearch || '').toLowerCase())
+ );
+
+ const handleChangetab = (i) => {
+ const tab = INVOICE_STATUS_TABS[i];
+ setValue(i);
+ setBillStatus(tab.billStatus);
+ setSearch('');
};
- useEffect(() => {
- filteredList();
- }, [search]);
+
+ // KPI tile definitions.
+ const kpiCards = INVOICE_STATUS_TABS.map((t) => ({
+ ...t,
+ value: insightdata?.[t.countKey] ?? 0
+ }));
+
return (
<>
{isloader && }
-
-
- Invoice
-
-
-
- {' '}
- {
- setValue(0);
- setBillStatus(0);
- setcontent('one');
- }}
- >
-
- {' '}
- {
- setValue(1);
- setBillStatus(1);
- setcontent('two');
- }}
- >
-
-
- {
- setValue(2);
- setBillStatus(2);
- setcontent('three');
- }}
- >
-
-
- {
- setValue(3);
- setBillStatus(3);
- setcontent('four');
- }}
- >
-
-
-
- {/* ============================================= || Invoice Table || ============================================= */}
-
-
-
- {
- setValue(0);
- setBillStatus(0);
- setcontent('one');
- }}
- />
- {
- setValue(1);
- setBillStatus(1);
- setcontent('two');
- }}
- />
- {
- setValue(2);
- setBillStatus(2);
- setcontent('three');
- }}
- />
- {
- setValue(3);
- setBillStatus(3);
- setcontent('four');
- }}
- />
-
-
-
-
-
- {/*
- setOpendate(true)}
- >
-
-
- */}
-
-
- }
- // secondary={}
- sx={{ mt: 3 }}
+
+ {/* ============================================= || Header || ============================================= */}
+
-
-
+
-
-
+
+
+
+ Invoices
+
+
+
+
+ Live · {INVOICE_STATUS_TABS[value].label}
+
+
+
+
+
+
+ {/* ============================================= || KPI Cards || ============================================= */}
+
+ {kpiCards.map((item) => {
+ const Icon = item.icon;
+ const active = value === item.idx;
+ return (
+
+ handleChangetab(item.idx)}
+ sx={{
+ cursor: 'pointer',
+ position: 'relative',
+ overflow: 'hidden',
+ p: { xs: 1.25, sm: 1.75, md: 2.25 },
+ borderRadius: DT.radiusCard / 8,
+ border: '1px solid',
+ borderColor: active ? edge(item.color) : DT.borderSubtle,
+ background: '#fff',
+ boxShadow: active ? `0 6px 24px ${ring(item.color)}` : 'none',
+ transition: 'transform 0.2s, box-shadow 0.2s, border-color 0.2s',
+ '&:hover': {
+ transform: 'translateY(-3px)',
+ boxShadow: DT.shadowMd,
+ borderColor: edge(item.color)
}
}}
>
-
- {columns.map((column) => (
-
+
+
+
- {column.label}
-
- ))}
+ {item.label} Invoices
+
+
+ {insightdata && insightdata[item.countKey] != null ? (
+ insightdata[item.countKey]
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ {/* ============================================= || Status Tabs + Search || ============================================= */}
+
+
+
+ {INVOICE_STATUS_TABS.map((t) => {
+ const Icon = t.icon;
+ const active = value === t.idx;
+ const count = insightdata?.[t.countKey] ?? 0;
+ return (
+ handleChangetab(t.idx)}
+ sx={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: { xs: 0.625, md: 0.875 },
+ pl: 0.5,
+ pr: { xs: 1, md: 1.25 },
+ py: 0.5,
+ flexShrink: 0,
+ cursor: 'pointer',
+ borderRadius: 999,
+ border: `1.5px solid ${active ? t.color : edge(t.color)}`,
+ bgcolor: active ? t.color : tint(t.color),
+ color: active ? '#fff' : t.color,
+ fontWeight: 700,
+ boxShadow: active ? `0 6px 18px ${ring(t.color)}` : 'none',
+ transition: 'all 0.18s',
+ '&:hover': {
+ borderColor: t.color,
+ boxShadow: active ? `0 6px 18px ${ring(t.color)}` : `0 0 0 3px ${ring(t.color)}`
+ }
+ }}
+ >
+
+
+
+
+ {t.label}
+
+
+ {count}
+
+
+ );
+ })}
+
+
+
+
+
+ setSearch(e.target.value)}
+ autoComplete="off"
+ sx={{
+ flex: 1,
+ fontSize: 13,
+ fontWeight: 600,
+ color: DT.textPrimary,
+ '& input::placeholder': { color: DT.textMuted, opacity: 1 }
+ }}
+ />
+ {search && (
+ setSearch('')} sx={{ p: 0.25, color: BRAND }}>
+
+
+ )}
+
+
+
+
+
+ {/* ============================================= || Table || ============================================= */}
+
+
+
+
+
+ #
+ Client
+ Invoice Id
+ Invoice Date
+ Due Date
+ Count
+ Amount
+ Action
+
+
+
+
+ {isloader && filteredList.length === 0 && (
+ [0, 1, 2, 3, 4].map((_, idx) => (
+
+ {Array.from({ length: 8 }).map((__, ci) => (
+
+
+
+ ))}
+
+ ))
+ )}
+
+ {!isloader && filteredList.length === 0 && (
+
+
+
+
+
+
+
+ No {INVOICE_STATUS_TABS[value].label.toLowerCase()} invoices
+
+
+ {search ? 'Try a different invoice number.' : 'Switch tabs above to load invoices.'}
+
+
+
-
-
- {deliveryList.length == 0 ? (
- <>
-
-
-
+ )}
+
+ {filteredList.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((item, index) => {
+ const tabMeta = INVOICE_STATUS_TABS[value];
+ return (
+ {
+ setIsLoader(true);
+ setTimeout(() => {
+ setIsLoader(false);
+ navigate('/nearle/invoice/preview', { state: item });
+ }, 300);
+ }}
+ >
+
+
+ {page * rowsPerPage + index + 1}
+
+
+
+
+
+ {item.tenantname}
+
+
+ {item.contactperson}
+
+
+
+
+
+ {item.invoiceno}
+
+
+
+
+
+
+
+
+ {item.transactiondate ? dayjs(item.transactiondate).format('DD MMM YYYY') : '—'}
+
+
+ {item.transactiondate ? dayjs(item.transactiondate).format('hh:mm A') : ''}
+
+
- >
- ) : (
- deliveryList.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((item, index) => (
- //
- // {columns.map((column) => {
- // const value = row[column.id];
- // return (
- //
- // {column.format && typeof value === "number"
- // ? column.format(value)
- // : value}
- //
- // );
- // })}
- //
-
- {index + 1}
-
- {item.tenantname}
+
+
+
+
+
+ {item.duedate ? dayjs(item.duedate).format('DD MMM YYYY') : '—'}
+
+
+ {item.duedate ? dayjs(item.duedate).format('hh:mm A') : ''}
+
+
+
+
-
- {item.contactperson}{' '}
-
-
-
- {item.invoiceno}
-
-
-
- {dayjs(item.transactiondate).format('DD-MM-YYYY')}
-
- {dayjs(item.transactiondate).format('hh:mm a')}
-
-
-
-
- {dayjs(item.duedate).format('DD-MM-YYYY')}
-
- {dayjs(item.duedate).format('hh:mm a')}
-
-
+
+
+ {item.itemcount ?? 0}
+
+
-
- {' '}
- {item.itemcount}
-
-
- {formatNumberToRupees(item.totalamount)}
-
-
+
+
+ {formatNumberToRupees(item.totalamount)}
+
+
+
+
+
{
+ size="small"
+ onClick={(e) => {
+ e.stopPropagation();
setIsLoader(true);
- console.log('selected', item);
- setselected(item);
setTimeout(() => {
setIsLoader(false);
- // setpredialog(true);
- navigate('/nearle/invoice/preview', {
- state: item
- });
- }, 500);
+ navigate('/nearle/invoice/preview', { state: item });
+ }, 300);
+ }}
+ sx={{
+ bgcolor: tint(BRAND),
+ border: `1px solid ${edge(BRAND)}`,
+ color: BRAND,
+ borderRadius: 999,
+ p: 0.75,
+ '&:hover': {
+ bgcolor: soft(BRAND),
+ borderColor: BRAND
+ }
}}
>
-
- {' '}
-
-
- ))
- )}
-
-
-
-
+
+
+
+
+
+ );
+ })}
+
+
+
-
- {/* table pagination */}
-
+
>
);
};
diff --git a/src/pages/nearle/locations/ResponsiveLocationDrawer.js b/src/pages/nearle/locations/ResponsiveLocationDrawer.js
index c514268..5db2686 100644
--- a/src/pages/nearle/locations/ResponsiveLocationDrawer.js
+++ b/src/pages/nearle/locations/ResponsiveLocationDrawer.js
@@ -131,8 +131,17 @@ const ResponsiveLocationDrawer = () => {
queryKey: ['locations', debouncedSearchLocation],
queryFn: gettenantlocations
});
+ // Auto-pick a sensible default whenever the locations list changes:
+ // • Nothing selected yet → pick the first item.
+ // • Current selection has been filtered out → also pick the first item
+ // (otherwise the orders panel queries a locationid that's no longer
+ // in the visible list, returning nothing and confusing the operator).
useEffect(() => {
- if (!searchLocation) locations?.length > 0 ? setSelectedLocation(locations[0]) : null;
+ if (!Array.isArray(locations) || locations.length === 0) return;
+ const stillVisible =
+ selectedLocation &&
+ locations.some((l) => l.locationid === selectedLocation.locationid);
+ if (!stillVisible) setSelectedLocation(locations[0]);
}, [locations]);
const {
@@ -334,7 +343,7 @@ const ResponsiveLocationDrawer = () => {
color: 'white' // text color
}}
>
- {row.locationname[0].toUpperCase()}
+ {row.locationname?.[0]?.toUpperCase() || '?'}
{' '}
@@ -535,8 +544,8 @@ const ResponsiveLocationDrawer = () => {
{row.pickupcustomer}
{row.pickupcontactno}
-
- {row.pickupsuburb || row.pickupaddress.slice(0, 20)}
+
+ {row.pickupsuburb || row.pickupaddress?.slice(0, 20) || '—'}
@@ -549,8 +558,8 @@ const ResponsiveLocationDrawer = () => {
{row.deliverycustomer}
{row.deliverycontactno}
-
- {row.deliverysuburb || row.deliveryaddress.slice(0, 20)}
+
+ {row.deliverysuburb || row.deliveryaddress?.slice(0, 20) || '—'}
diff --git a/src/pages/nearle/login.js b/src/pages/nearle/login.js
index 32f64c9..b315678 100644
--- a/src/pages/nearle/login.js
+++ b/src/pages/nearle/login.js
@@ -42,7 +42,7 @@ const Login = () => {
useEffect(() => {
if (localStorage.getItem('authname')) {
- navigate('/nearle/orders');
+ navigate('/nearle/dispatch');
}
}, []);
@@ -79,7 +79,7 @@ const Login = () => {
setSubmitting(false);
setLoading(false);
- navigate('/nearle/orders');
+ navigate('/nearle/dispatch');
} else {
OpenToast(res.data.message, 'warning', 2000);
setLoading(false);
diff --git a/src/pages/nearle/orders/OrdersRedesign.css b/src/pages/nearle/orders/OrdersRedesign.css
new file mode 100644
index 0000000..cc2c39c
--- /dev/null
+++ b/src/pages/nearle/orders/OrdersRedesign.css
@@ -0,0 +1,288 @@
+/* OrdersRedesign.css — premium aesthetics + micro-animations
+ Ported from xpressconsole; trimmed to only the classes the
+ redesigned multipleOrders.js (and its peers) consume here. */
+
+/* ---------- Page background ---------- */
+.orders-workspace-bg {
+ background: linear-gradient(135deg, #f8fafc 0%, #edf2f7 100%) !important;
+}
+
+/* ---------- Cards ---------- */
+.orders-card {
+ background: #ffffff !important;
+ border: 1px solid #eef2f6 !important;
+ border-radius: 12px !important;
+ box-shadow: 0 10px 25px -5px rgba(62, 73, 84, 0.04),
+ 0 4px 12px -2px rgba(62, 73, 84, 0.02) !important;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
+ overflow: hidden;
+}
+.orders-card:hover {
+ box-shadow: 0 16px 35px -8px rgba(62, 73, 84, 0.08),
+ 0 6px 16px -3px rgba(62, 73, 84, 0.03) !important;
+}
+
+.orders-card .MuiOutlinedInput-root {
+ font-size: 13px !important;
+ border-radius: 10px !important;
+}
+.orders-card .MuiOutlinedInput-input {
+ font-size: 13px !important;
+ padding-top: 9px !important;
+ padding-bottom: 9px !important;
+}
+.orders-card .MuiInputLabel-root {
+ font-size: 13px !important;
+}
+.orders-card .MuiInputLabel-root.MuiInputLabel-shrink {
+ font-size: 11.5px !important;
+}
+
+/* ---------- Section title bar (gradient strip + h-title) ---------- */
+.section-title-bar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 20px;
+}
+.section-title-bar::before {
+ content: '';
+ width: 4px;
+ height: 22px;
+ border-radius: 4px;
+ background: linear-gradient(180deg, #1890ff, #096dd9);
+ flex-shrink: 0;
+}
+.section-title-bar--accent::before {
+ background: linear-gradient(180deg, #a855f7, #65387a);
+}
+.section-title-bar .MuiTypography-root {
+ margin-bottom: 0 !important;
+ line-height: 1.2;
+}
+
+/* ---------- Delivery preferences card ---------- */
+.delivery-prefs-card {
+ background: linear-gradient(135deg, #ffffff 0%, #fbfcff 100%) !important;
+ border: 1px solid #eef2f6 !important;
+ border-radius: 14px !important;
+}
+.delivery-prefs-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 10px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #f1f5f9;
+}
+.delivery-prefs-title {
+ font-size: 14px !important;
+ font-weight: 700 !important;
+ color: #1e293b !important;
+ letter-spacing: -0.01em;
+ line-height: 1.2;
+}
+.delivery-prefs-sub {
+ font-size: 10.5px !important;
+ font-weight: 500 !important;
+ color: #94a3b8 !important;
+ text-transform: uppercase;
+ letter-spacing: 0.55px;
+ text-align: right;
+ line-height: 1.2;
+}
+.delivery-prefs-row {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.delivery-prefs-field {
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+}
+.delivery-prefs-label {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 10.5px;
+ font-weight: 700;
+ letter-spacing: 0.55px;
+ text-transform: uppercase;
+ color: #64748b;
+}
+
+/* ---------- Pricing summary card ---------- */
+.pricing-summary-card {
+ background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%) !important;
+ border: 1px solid #eef2f6 !important;
+ border-radius: 14px !important;
+ padding: 14px 16px !important;
+}
+.pricing-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 10px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #f1f5f9;
+}
+.pricing-title {
+ font-size: 14px !important;
+ font-weight: 700 !important;
+ color: #1e293b !important;
+ letter-spacing: -0.01em;
+}
+.pricing-subtitle {
+ font-size: 11px !important;
+ font-weight: 500 !important;
+ color: #94a3b8 !important;
+ text-transform: uppercase;
+ letter-spacing: 0.6px;
+}
+
+/* ---------- Total charge badge ---------- */
+.total-charge-badge {
+ background: linear-gradient(135deg, rgba(24, 144, 255, 0.08) 0%, rgba(101, 56, 122, 0.10) 100%);
+ border: 1px solid rgba(101, 56, 122, 0.18);
+ border-radius: 10px;
+ padding: 10px 14px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-top: 12px;
+}
+.total-charge-left {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+.total-charge-icon {
+ font-size: 13px;
+ color: #65387A;
+ flex-shrink: 0;
+}
+.total-charge-label {
+ font-size: 11.5px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.6px;
+ color: #65387A;
+}
+.total-charge-val {
+ font-size: 20px;
+ font-weight: 800;
+ color: #65387A;
+ line-height: 1.1;
+ letter-spacing: -0.01em;
+ white-space: nowrap;
+}
+
+/* ---------- Gradient action button ---------- */
+.gradient-btn-create {
+ background: linear-gradient(135deg, #1890ff 0%, #65387a 100%) !important;
+ color: #ffffff !important;
+ font-weight: 600 !important;
+ font-size: 13px !important;
+ letter-spacing: 0.01em !important;
+ text-transform: none !important;
+ border-radius: 10px !important;
+ padding: 8px 18px !important;
+ min-height: 38px !important;
+ box-shadow: 0 4px 12px -3px rgba(24, 144, 255, 0.30),
+ 0 2px 4px rgba(101, 56, 122, 0.10) !important;
+ transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1) !important;
+ border: none !important;
+ display: inline-flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ gap: 8px !important;
+}
+.gradient-btn-create .MuiButton-startIcon,
+.gradient-btn-create .MuiButton-endIcon {
+ margin: 0 !important;
+}
+.gradient-btn-create:hover {
+ filter: brightness(1.04);
+ box-shadow: 0 8px 18px -4px rgba(24, 144, 255, 0.40),
+ 0 3px 8px rgba(101, 56, 122, 0.18) !important;
+}
+.gradient-btn-create:active { filter: brightness(0.98); }
+.gradient-btn-create.Mui-disabled,
+.gradient-btn-create:disabled {
+ background: #e2e8f0 !important;
+ color: #94a3b8 !important;
+ box-shadow: none !important;
+ cursor: not-allowed !important;
+}
+
+/* ---------- Compact header inputs (Location/Client/Business) ---------- */
+.header-compact-tf .MuiOutlinedInput-root {
+ border-radius: 10px !important;
+ height: 40px !important;
+ padding-left: 10px !important;
+ font-size: 12.5px !important;
+ background: #ffffff;
+}
+.header-compact-tf .MuiOutlinedInput-input {
+ padding-top: 6px !important;
+ padding-bottom: 6px !important;
+ font-size: 12.5px !important;
+}
+.header-compact-tf .MuiInputLabel-root {
+ font-size: 11.5px !important;
+ letter-spacing: 0.02em;
+ font-weight: 600;
+ color: #64748b !important;
+}
+.header-compact-tf .MuiInputLabel-shrink {
+ transform: translate(12px, -7px) scale(0.82) !important;
+ background: #ffffff;
+ padding: 0 4px;
+}
+.header-compact-tf .MuiOutlinedInput-notchedOutline { border-color: #e2e8f0; }
+.header-compact-tf:hover .MuiOutlinedInput-notchedOutline { border-color: #cbd5e1; }
+.header-compact-tf .Mui-focused .MuiOutlinedInput-notchedOutline { border-width: 1.5px !important; }
+
+.header-compact-input .MuiAutocomplete-endAdornment {
+ top: 50%;
+ transform: translateY(-50%);
+ right: 8px;
+ display: inline-flex;
+ align-items: center;
+ height: auto;
+ gap: 2px;
+}
+.header-compact-input .MuiAutocomplete-endAdornment .MuiSvgIcon-root {
+ font-size: 16px;
+ display: block;
+}
+.header-compact-input .MuiAutocomplete-clearIndicator,
+.header-compact-input .MuiAutocomplete-popupIndicator {
+ padding: 3px !important;
+ width: 22px;
+ height: 22px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: #94a3b8 !important;
+}
+.header-compact-input .MuiAutocomplete-clearIndicator:hover,
+.header-compact-input .MuiAutocomplete-popupIndicator:hover {
+ background: rgba(148, 163, 184, 0.12) !important;
+ color: #475569 !important;
+}
+.header-compact-input .MuiAutocomplete-popupIndicator { margin-right: 0; }
+.header-compact-input .MuiOutlinedInput-root {
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+ padding-right: 60px !important;
+}
+.header-compact-input .MuiAutocomplete-input {
+ padding: 4px 4px 4px 0 !important;
+ height: auto !important;
+}
diff --git a/src/pages/nearle/orders/createorder1.js b/src/pages/nearle/orders/createorder1.js
index c9d7e99..0e000e1 100644
--- a/src/pages/nearle/orders/createorder1.js
+++ b/src/pages/nearle/orders/createorder1.js
@@ -67,6 +67,114 @@ import { SearchOutlined, CloseOutlined } from '@ant-design/icons';
import PhoneInput from 'react-phone-number-input/input';
import MyLocationIcon from '@mui/icons-material/MyLocation';
import HighlightOffIcon from '@mui/icons-material/HighlightOff';
+import { Paper } from '@mui/material';
+import {
+ MdAddShoppingCart,
+ MdMyLocation,
+ MdLocationOn,
+ MdSchedule,
+ MdLocalShipping,
+ MdAttachMoney,
+ MdNotes,
+ MdCheckCircle,
+ MdReceiptLong,
+ MdInventory2,
+ MdStraighten,
+ MdPersonPin,
+ MdPerson,
+ MdHistoryToggleOff
+} from 'react-icons/md';
+
+// ============================================================================
+// Design tokens — shared with the rest of the redesigned operator pages.
+// ============================================================================
+const DT = {
+ radiusPill: 999,
+ radiusCard: 16,
+ shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
+ shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
+ shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
+ textPrimary: '#0f172a',
+ textSecondary: '#64748b',
+ textMuted: '#94a3b8',
+ borderSubtle: '#e2e8f0',
+ divider: '#f1f5f9',
+ surface: '#ffffff',
+ surfaceAlt: '#f8fafc'
+};
+const dtA = (c, suffix) => `${c}${suffix}`;
+const tint = (c) => dtA(c, '08');
+const soft = (c) => dtA(c, '18');
+const ring = (c) => dtA(c, '26');
+const edge = (c) => dtA(c, '55');
+
+const BRAND = '#662582';
+const BRAND_LIGHT = '#9255AB';
+
+// Soft card section header — coloured banner above each form section.
+const SectionHeader = ({ color, icon, title, subtitle, action }) => (
+
+
+
+ {icon}
+
+
+ {title}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+
+ {action}
+
+);
+
+// Section wrapper Paper used as the outer card for each form block.
+const SectionCard = ({ children, sx = {} }) => (
+
+ {children}
+
+);
+
+// Popup paper for Autocomplete dropdowns — matches the rest of the design.
+const SoftPaper = (props) => (
+
+);
function loadScript(src, position, id) {
if (!position) {
@@ -680,18 +788,20 @@ const Createorder1 = () => {
useEffect(() => {
fetchtenantinfo();
}, []);
- // ==================================================== || getsubcategories || ====================================================
const getsubcategories = async () => {
await axios
.get(`${process.env.REACT_APP_URL}/utils/getsubcategories/?moduleid=6`)
.then((res) => {
console.log('subcateRes', res.data.details);
- if (res.data.status) {
+ if (res.data.status && res.data.details) {
setSubCat(res.data.details);
+ } else {
+ setSubCat([]);
}
})
.catch((err) => {
console.log(err);
+ setSubCat([]);
});
};
useEffect(() => {
@@ -734,7 +844,7 @@ const Createorder1 = () => {
};
useEffect(() => {
fetchTiming();
- }, [starttime, endtime]);
+ }, []);
// =============================================== || fetchAppAdminTokens (via appId) || ===============================================
const fetchAppAdminTokens = async () => {
@@ -979,16 +1089,16 @@ const Createorder1 = () => {
let url =
searchCustList == ''
? // ? `${process.env.REACT_APP_URL}/customers/getbytid/?tenantid=${tid}&pageno=1&pagesize=1`
- `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tid}&pageno=1&pagesize=20`
+ `${process.env.REACT_APP_URL}/customers/gettenantcustomers/?tenantid=${tid}&pageno=1&pagesize=20`
: `${process.env.REACT_APP_URL}/customers/search/?tenantid=${tid}&keyword=${searchCustList}`;
await axios
.get(url)
.then((res) => {
console.log('clientdetails', res.data.details);
- if (res.data.status) {
- setClientdetail(res.data.details);
- setCustomerlist(res.data.details);
+ if (res.data.status && res.data.details) {
+ setClientdetail(res.data.details || []);
+ setCustomerlist(res.data.details || []);
let arr = [];
res.data.details.map((val) => {
arr.push({
@@ -997,6 +1107,10 @@ const Createorder1 = () => {
});
});
setClientdetailarr(arr);
+ } else {
+ setClientdetail([]);
+ setCustomerlist([]);
+ setClientdetailarr([]);
}
setLoading2(false);
})
@@ -1263,13 +1377,18 @@ const Createorder1 = () => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${tid}`);
console.log('gettenantlocations', res.data.details);
- setTenantlocations(res.data.details);
- if (res.data.details.length == 1) {
- setIsLocation(true);
- setTenanatLocoId(res.data.details[0].locationid);
+ if (res.data && res.data.details) {
+ setTenantlocations(res.data.details);
+ if (res.data.details.length == 1) {
+ setIsLocation(true);
+ setTenanatLocoId(res.data.details[0].locationid);
+ }
+ } else {
+ setTenantlocations([]);
}
} catch (err) {
console.log('gettenantlocations', err);
+ setTenantlocations([]);
}
};
useEffect(() => {
@@ -1280,188 +1399,299 @@ const Createorder1 = () => {
<>
{loading && }
-
-
-
+
+
+
- Create Order
- {tenantLocations.length == 1 ? (
-
-
-
- )
+
+
+ >
+
+
+
+
+ Create Order
+
+
+
+
+ Fill pickup, drop, schedule & pricing details
+
+
+
+
+
+ {tenantLocations.length === 1 ? (
+
+ {tenantLocations[0].locationname}
+
) : (
`${option.locationname} (${option.suburb})` || ''}
- sx={{ width: 300 }}
- renderInput={(params) => }
+ getOptionLabel={(option) =>
+ option && option.locationname ? `${option.locationname} (${option.suburb || ''})` : ''
+ }
+ isOptionEqualToValue={(option, value) => option?.locationid === value?.locationid}
+ PaperComponent={SoftPaper}
+ sx={{ width: { xs: '100%', sm: 300 } }}
+ size="small"
+ renderInput={(params) => (
+
+
+
+
+
+ )
+ }}
+ sx={{
+ '& .MuiOutlinedInput-root': {
+ borderRadius: DT.radiusPill + 'px',
+ bgcolor: '#fff',
+ fontWeight: 600,
+ '& fieldset': { borderColor: edge(BRAND), borderWidth: 1.5 },
+ '&:hover fieldset': { borderColor: BRAND },
+ '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` },
+ '&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 2 }
+ }
+ }}
+ />
+ )}
onChange={(event, value, reason) => {
if (value) {
- console.log('locationid', value.locationid);
setTenanatLocoId(value.locationid);
setIsLocation(true);
}
- if (reason == 'clear') {
+ if (reason === 'clear') {
setIsLocation(false);
}
}}
/>
)}
-
-
+
+
-
-
-
+
+
+
{/* ================================================= || Pickup || ================================================= */}
-
-
-
-
-
- Pickup Details
-
-
- {
- if (!isLocation) {
- opentoast('Select Business Location', 'warning');
- } else {
- setIsCustomerOpen(true);
- setpickordrop(1);
- setPickCust({});
-
- setInputValue2('');
- setSearchCustList('');
- }
- }}
- >
- Saved Locations
-
-
-
-
-
-
-
- {/* ====================================== ||Contact Name (pick) || ====================================== */}
-
-
-
-
- )
- }}
- variant="outlined"
- label="Contact Name"
- value={pickCust.firstname}
- onChange={(e) => {
- setPickCust({ ...pickCust, firstname: e.target.value });
- }}
- />
-
- {/* ====================================== ||Contact Number(pick) || ====================================== */}
-
-
-
-
- )
- }}
- variant="outlined"
- label="Contact Number"
- value={pickCust.contactno}
- onChange={(e) => {
- if (e.target.value.length <= 10) {
- setPickCust({ ...pickCust, contactno: e.target.value });
- }
- if (pickNum == e.target.value) {
- setShowCheck1(0);
- } else {
- setShowCheck1(1);
- }
- if (e.target.value.length < 10) {
- setNumErr1(true);
- } else {
- setNumErr1(false);
- }
- }}
- />
-
- {/* ====================================== || Address (pick) || ====================================== */}
-
-
- {addId1 == 0 ? (
-
+
+ }
+ title="Pickup Details"
+ subtitle="Where the order is picked up"
+ action={
+ }
+ onClick={() => {
+ if (!isLocation) {
+ opentoast('Select Business Location', 'warning');
+ } else {
+ setIsCustomerOpen(true);
+ setpickordrop(1);
+ setPickCust({});
+ setInputValue2('');
+ setSearchCustList('');
+ }
+ }}
+ sx={{
+ borderRadius: 999,
+ px: 1.5,
+ py: 0.375,
+ fontSize: 11.5,
+ fontWeight: 700,
+ textTransform: 'none',
+ borderColor: edge('#0ea5e9'),
+ color: '#0ea5e9',
+ '&:hover': {
+ borderColor: '#0ea5e9',
+ bgcolor: tint('#0ea5e9'),
+ boxShadow: `0 0 0 3px ${ring('#0ea5e9')}`
+ }
+ }}
+ >
+ Saved Locations
+
+ }
+ />
+
+
+
+
+ {/* ====================================== ||Contact Name (pick) || ====================================== */}
+
+
+
+
+ )
+ }}
+ variant="outlined"
+ label="Contact Name"
+ value={pickCust.firstname}
+ onChange={(e) => {
+ setPickCust({ ...pickCust, firstname: e.target.value });
+ }}
+ />
+
+ {/* ====================================== ||Contact Number(pick) || ====================================== */}
+
+
+
+
+ )
+ }}
+ variant="outlined"
+ label="Contact Number"
+ value={pickCust.contactno}
+ onChange={(e) => {
+ if (e.target.value.length <= 10) {
+ setPickCust({ ...pickCust, contactno: e.target.value });
+ }
+ if (pickNum == e.target.value) {
+ setShowCheck1(0);
+ } else {
+ setShowCheck1(1);
+ }
+ if (e.target.value.length < 10) {
+ setNumErr1(true);
+ } else {
+ setNumErr1(false);
+ }
+ }}
+ />
+
+ {/* ====================================== || Address (pick) || ====================================== */}
+
+
+ {addId1 == 0 ? (
+
+ setInputValue2(e.target.value)}
+ InputProps={{
+ endAdornment: (
+ {
+ setInputValue2('');
+ setPickCust({
+ ...pickCust,
+ doorno: '',
+ suburb: '',
+ city: '',
+ postcode: '',
+ landmark: ''
+ });
+ setShowDistance(false);
+ setStartPoint({ latitude: 0, longitude: 0 });
+ }}
+ size="small"
+ >
+
+
+ )
+ }}
+ />
+
+ ) : (
setInputValue2(e.target.value)}
InputProps={{
endAdornment: (
{
- setInputValue2('');
+ setAddId1(0);
setPickCust({
...pickCust,
+ // firstname: '',
+ // contactno: '',
doorno: '',
suburb: '',
city: '',
@@ -1471,210 +1701,204 @@ const Createorder1 = () => {
setShowDistance(false);
setStartPoint({ latitude: 0, longitude: 0 });
}}
- size="small"
>
-
+
)
}}
+ variant="outlined"
+ placeholder="Select"
+ value={pickCust.address}
+ onChange={(e) => {
+ setPickCust({ ...pickCust, address: e.target.value });
+ if (e.target.value == '') {
+ setAddId1(0);
+ setShowDistance(false);
+ setStartPoint({ latitude: 0, longitude: 0 });
+ }
+ }}
/>
-
- ) : (
- {
- setAddId1(0);
- setPickCust({
- ...pickCust,
- // firstname: '',
- // contactno: '',
- doorno: '',
- suburb: '',
- city: '',
- postcode: '',
- landmark: ''
- });
- setShowDistance(false);
- setStartPoint({ latitude: 0, longitude: 0 });
- }}
- >
-
-
- )
- }}
- variant="outlined"
- placeholder="Select"
- value={pickCust.address}
- onChange={(e) => {
- setPickCust({ ...pickCust, address: e.target.value });
- if (e.target.value == '') {
- setAddId1(0);
- setShowDistance(false);
- setStartPoint({ latitude: 0, longitude: 0 });
- }
- }}
- />
- )}
-
-
-
- {/* ====================================== ||Door No (pick) || ====================================== */}
-
-
-
-
- )
- }}
- variant="outlined"
- label="Door No / Street"
- value={pickCust.doorno}
- onChange={(e) => {
- setPickCust({ ...pickCust, doorno: e.target.value });
- }}
- />
-
- {/* ====================================== || Suburb (pick) || ====================================== */}
-
-
-
-
- )
- }}
- variant="outlined"
- label="Location"
- value={pickCust.suburb}
- onChange={(e) => {
- setPickCust({ ...pickCust, suburb: e.target.value });
- }}
- />
-
- {/* ====================================== || City (pick) || ====================================== */}
-
-
-
-
- )
- }}
- variant="outlined"
- label="City"
- value={pickCust.city}
- onChange={(e) => {
- setPickCust({ ...pickCust, city: e.target.value });
- }}
- />
-
- {/* ====================================== || postcode (pick) || ====================================== */}
-
-
-
-
- )
- }}
- variant="outlined"
- label="Postcode"
- value={pickCust.postcode}
- onChange={(e) => {
- setPickCust({ ...pickCust, postcode: e.target.value });
- }}
- />
-
- {/* ====================================== || Landmark (pick) || ====================================== */}
-
-
-
-
- )
- }}
- variant="outlined"
- label="Landmark"
- value={pickCust.landmark}
- onChange={(e) => {
- setPickCust({ ...pickCust, landmark: e.target.value });
- }}
- />
-
- {/* ====================================== ||Checkbox save for later (pick) || ====================================== */}
- {showCheck1 == 1 && (
-
-
- {
- setIsNumChange1(e.target.checked ? 1 : 0);
- }}
- />
- }
- label="Save For Later"
- />
-
+ )}
+
- )}
+
+ {/* ====================================== ||Door No (pick) || ====================================== */}
+
+
+
+
+ )
+ }}
+ variant="outlined"
+ label="Door No / Street"
+ value={pickCust.doorno}
+ onChange={(e) => {
+ setPickCust({ ...pickCust, doorno: e.target.value });
+ }}
+ />
+
+ {/* ====================================== || Suburb (pick) || ====================================== */}
+
+
+
+
+ )
+ }}
+ variant="outlined"
+ label="Location"
+ value={pickCust.suburb}
+ onChange={(e) => {
+ setPickCust({ ...pickCust, suburb: e.target.value });
+ }}
+ />
+
+ {/* ====================================== || City (pick) || ====================================== */}
+
+
+
+
+ )
+ }}
+ variant="outlined"
+ label="City"
+ value={pickCust.city}
+ onChange={(e) => {
+ setPickCust({ ...pickCust, city: e.target.value });
+ }}
+ />
+
+ {/* ====================================== || postcode (pick) || ====================================== */}
+
+
+
+
+ )
+ }}
+ variant="outlined"
+ label="Postcode"
+ value={pickCust.postcode}
+ onChange={(e) => {
+ setPickCust({ ...pickCust, postcode: e.target.value });
+ }}
+ />
+
+ {/* ====================================== || Landmark (pick) || ====================================== */}
+
+
+
+
+ )
+ }}
+ variant="outlined"
+ label="Landmark"
+ value={pickCust.landmark}
+ onChange={(e) => {
+ setPickCust({ ...pickCust, landmark: e.target.value });
+ }}
+ />
+
+ {/* ====================================== ||Checkbox save for later (pick) || ====================================== */}
+ {showCheck1 == 1 && (
+
+
+ {
+ setIsNumChange1(e.target.checked ? 1 : 0);
+ }}
+ />
+ }
+ label="Save For Later"
+ />
+
+
+ )}
+
- {/*
- {
- console.log('pickCust', pickCust);
- console.log('dropCust', dropCust);
- }}
- >
- Save for Later
-
- */}
-
-
+
+
{/* ================================================= || Drop || ================================================= */}
-
-
-
-
-
- Drop Details
-
-
- {/* Customer */}
- {/*
+ }
+ title="Drop Details"
+ subtitle="Where the order is delivered"
+ action={
+ }
+ onClick={() => {
+ if (!isLocation) {
+ opentoast('Select Business Location', 'warning');
+ } else {
+ setIsCustomerOpen(true);
+ setpickordrop(2);
+ setInputValue3('');
+ setSearchCustList('');
+ }
+ }}
+ sx={{
+ borderRadius: 999,
+ px: 1.5,
+ py: 0.375,
+ fontSize: 11.5,
+ fontWeight: 700,
+ textTransform: 'none',
+ borderColor: edge(BRAND),
+ color: BRAND,
+ '&:hover': {
+ borderColor: BRAND,
+ bgcolor: tint(BRAND),
+ boxShadow: `0 0 0 3px ${ring(BRAND)}`
+ }
+ }}
+ >
+ Saved Locations
+
+ }
+ />
+
+
+
+
+
+ Drop Details
+
+
+ {/* Customer */}
+ {/* {
if (val) {
@@ -1688,32 +1912,32 @@ const Createorder1 = () => {
size="small"
/>
Business */}
- {
- if (!isLocation) {
- opentoast('Select Business Location', 'warning');
- } else {
- setIsCustomerOpen(true);
- setpickordrop(2);
+ {
+ if (!isLocation) {
+ opentoast('Select Business Location', 'warning');
+ } else {
+ setIsCustomerOpen(true);
+ setpickordrop(2);
- setInputValue3('');
- setSearchCustList('');
- }
- }}
- >
- Saved Locations
-
+ setInputValue3('');
+ setSearchCustList('');
+ }
+ }}
+ >
+ Saved Locations
+
+
-
- {/* new2 */}
- {/* {
) : null
}
/> */}
-
+
-
-
- {/* ====================================== ||Contact Name (drop) || ====================================== */}
-
-
-
-
- )
- }}
- value={dropCust.firstname}
- onChange={(e) => {
- setDropCust({ ...dropCust, firstname: e.target.value });
- }}
- />
-
- {/* ====================================== ||Contact Number (drop) || ====================================== */}
-
-
-
-
- )
- }}
- value={dropCust.contactno}
- onChange={(e) => {
- if (e.target.value.length <= 10) {
- setDropCust({ ...dropCust, contactno: e.target.value });
- }
- if (dropNum == e.target.value) {
- setShowCheck2(0);
- } else {
- setShowCheck2(1);
- }
- if (e.target.value.length < 10) {
- setNumErr2(true);
- } else {
- setNumErr2(false);
- }
- }}
- />
-
-
-
- {addId2 == 0 ? (
-
+
+
+ {/* ====================================== ||Contact Name (drop) || ====================================== */}
+
+
+
+
+ )
+ }}
+ value={dropCust.firstname}
+ onChange={(e) => {
+ setDropCust({ ...dropCust, firstname: e.target.value });
+ }}
+ />
+
+ {/* ====================================== ||Contact Number (drop) || ====================================== */}
+
+
+
+
+ )
+ }}
+ value={dropCust.contactno}
+ onChange={(e) => {
+ if (e.target.value.length <= 10) {
+ setDropCust({ ...dropCust, contactno: e.target.value });
+ }
+ if (dropNum == e.target.value) {
+ setShowCheck2(0);
+ } else {
+ setShowCheck2(1);
+ }
+ if (e.target.value.length < 10) {
+ setNumErr2(true);
+ } else {
+ setNumErr2(false);
+ }
+ }}
+ />
+
+
+
+ {addId2 == 0 ? (
+
+ setInputValue3(e.target.value)}
+ InputProps={{
+ endAdornment: (
+ {
+ setInputValue3('');
+ setDropCust({
+ ...dropCust,
+ doorno: '',
+ suburb: '',
+ city: '',
+ postcode: '',
+ landmark: ''
+ });
+ setShowDistance(false);
+ setEndPoint({ latitude: 0, longitude: 0 });
+ }}
+ size="small"
+ >
+
+
+ )
+ }}
+ />
+
+ ) : (
setInputValue3(e.target.value)}
InputProps={{
endAdornment: (
{
- setInputValue3('');
+ setAddId2(0);
setDropCust({
...dropCust,
+ firstname: '',
+ contactno: '',
doorno: '',
suburb: '',
city: '',
@@ -1860,178 +2113,149 @@ const Createorder1 = () => {
setShowDistance(false);
setEndPoint({ latitude: 0, longitude: 0 });
}}
- size="small"
>
-
+
)
}}
+ variant="outlined"
+ placeholder="Select"
+ value={dropCust.address}
+ onChange={(e) => {
+ setPickCust({ ...dropCust, address: e.target.value });
+ if (e.target.value == '') {
+ setAddId2(0);
+ setShowDistance(false);
+ setEndPoint({ latitude: 0, longitude: 0 });
+ }
+ }}
/>
-
- ) : (
- {
- setAddId2(0);
- setDropCust({
- ...dropCust,
- firstname: '',
- contactno: '',
- doorno: '',
- suburb: '',
- city: '',
- postcode: '',
- landmark: ''
- });
- setShowDistance(false);
- setEndPoint({ latitude: 0, longitude: 0 });
- }}
- >
-
-
- )
- }}
- variant="outlined"
- placeholder="Select"
- value={dropCust.address}
- onChange={(e) => {
- setPickCust({ ...dropCust, address: e.target.value });
- if (e.target.value == '') {
- setAddId2(0);
- setShowDistance(false);
- setEndPoint({ latitude: 0, longitude: 0 });
- }
- }}
- />
- )}
-
-
-
- {/* ====================================== ||Door No (drop) || ====================================== */}
-
-
-
-
- )
- }}
- value={dropCust.doorno}
- onChange={(e) => {
- setDropCust({ ...dropCust, doorno: e.target.value });
- }}
- />
-
- {/* ====================================== ||Suburb (drop) || ====================================== */}
-
-
-
-
- )
- }}
- value={dropCust.suburb}
- onChange={(e) => {
- setDropCust({ ...dropCust, suburb: e.target.value });
- }}
- />
-
- {/* ====================================== ||City (drop) || ====================================== */}
-
-
-
-
- )
- }}
- value={dropCust.city}
- onChange={(e) => {
- setDropCust({ ...dropCust, city: e.target.value });
- }}
- />
-
-
- {/* ====================================== ||Postcode (drop) || ====================================== */}
-
-
-
-
- )
- }}
- value={dropCust.postcode}
- onChange={(e) => {
- setDropCust({ ...dropCust, postcode: e.target.value });
- }}
- />
-
- {/* ====================================== ||Landmark (drop) || ====================================== */}
-
-
-
-
- )
- }}
- value={dropCust.landmark}
- onChange={(e) => {
- setDropCust({ ...dropCust, landmark: e.target.value });
- }}
- />
-
- {/* ====================================== ||Checkbox save for later (drop) || ====================================== */}
- {showCheck2 == 1 && (
-
-
- {
- setIsNumChange2(e.target.checked ? 1 : 0);
- }}
- />
- }
- label="Save For Later"
- />
-
+ )}
+
- )}
+
+ {/* ====================================== ||Door No (drop) || ====================================== */}
+
+
+
+
+ )
+ }}
+ value={dropCust.doorno}
+ onChange={(e) => {
+ setDropCust({ ...dropCust, doorno: e.target.value });
+ }}
+ />
+
+ {/* ====================================== ||Suburb (drop) || ====================================== */}
+
+
+
+
+ )
+ }}
+ value={dropCust.suburb}
+ onChange={(e) => {
+ setDropCust({ ...dropCust, suburb: e.target.value });
+ }}
+ />
+
+ {/* ====================================== ||City (drop) || ====================================== */}
+
+
+
+
+ )
+ }}
+ value={dropCust.city}
+ onChange={(e) => {
+ setDropCust({ ...dropCust, city: e.target.value });
+ }}
+ />
+
+
+ {/* ====================================== ||Postcode (drop) || ====================================== */}
+
+
+
+
+ )
+ }}
+ value={dropCust.postcode}
+ onChange={(e) => {
+ setDropCust({ ...dropCust, postcode: e.target.value });
+ }}
+ />
+
+ {/* ====================================== ||Landmark (drop) || ====================================== */}
+
+
+
+
+ )
+ }}
+ value={dropCust.landmark}
+ onChange={(e) => {
+ setDropCust({ ...dropCust, landmark: e.target.value });
+ }}
+ />
+
+ {/* ====================================== ||Checkbox save for later (drop) || ====================================== */}
+ {showCheck2 == 1 && (
+
+
+ {
+ setIsNumChange2(e.target.checked ? 1 : 0);
+ }}
+ />
+ }
+ label="Save For Later"
+ />
+
+
+ )}
+
-
- {/*
{
Save for Later
*/}
-
-
+
+
+
{/* ================================================= || Time || ================================================= */}
-
-
-
- Date
-
- {
- let dateres11 = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd');
- console.log('dateres11');
- console.log(dateres11);
- setSelectedtime('');
- if (dateres11 <= 0) {
- console.log('startdate', e);
- setStartdate(e);
- setEnddate(e);
+
+ }
+ title="Schedule"
+ subtitle="Pickup date & time slot"
+ />
+
+
+
+ Date
+
+ {
+ let dateres11 = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd');
+ console.log('dateres11');
+ console.log(dateres11);
+ setSelectedtime('');
+ if (dateres11 <= 0) {
+ console.log('startdate', e);
+ setStartdate(e);
+ setEnddate(e);
- let arr = [];
- timeslotarr.map((val) => {
- if (
- dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0
- ) {
- arr.push(val);
- }
- });
- if (arr[0]) {
- setOrderarr([
- {
- sno: 1,
- address: '',
- customerid: '',
- deliverytime: dayjs(arr[0]) || '',
- deliverylocationid: '',
- clientname: '',
- contactno: '',
- latitude: '',
- longitude: ''
+ let arr = [];
+ timeslotarr.map((val) => {
+ if (
+ dayjs().diff(dayjs(`${dayjs(e).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0
+ ) {
+ arr.push(val);
}
- ]);
+ });
+ if (arr[0]) {
+ setOrderarr([
+ {
+ sno: 1,
+ address: '',
+ customerid: '',
+ deliverytime: dayjs(arr[0]) || '',
+ deliverylocationid: '',
+ clientname: '',
+ contactno: '',
+ latitude: '',
+ longitude: ''
+ }
+ ]);
+ } else {
+ setOrderarr([]);
+ }
} else {
- setOrderarr([]);
+ setAlertmessage('choose Upcoming Date');
+ opentoast('choose Upcoming Date', 'warning');
+ setStartdate(NaN);
}
- } else {
- setAlertmessage('choose Upcoming Date');
- opentoast('choose Upcoming Date', 'warning');
- setStartdate(NaN);
- }
+ }}
+ value={dayjs(startdate)}
+ sx={{ width: '100%', mt: 2 }}
+ disablePast
+ />
+
+
+ Time
+
+
-
-
- Time
-
-
-
- {timeslotarr.map((val, index) => {
- if (
- dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0
- ) {
- return (
-
-
- {
- if (distance > appLocaRadius) {
- setOpen4(true);
- } else if (showDistance) {
- console.log('selectedtime', val);
- setSelectedtime(val);
- } else {
- opentoast('Select Pickup and Drop', 'error');
- }
- }}
- />
-
-
- );
- }
- })}
-
-
+ >
+
+ {timeslotarr.map((val, index) => {
+ if (
+ dayjs().diff(dayjs(`${dayjs(startdate).format('MM-DD-YYYY')} ${dayjs(val).format('HH:mm:ss')}`), 'm') <= 0
+ ) {
+ const active = dayjs(selectedtime).format('HH:mm') == dayjs(val).format('HH:mm');
+ return (
+ {
+ if (distance > appLocaRadius) {
+ setOpen4(true);
+ } else if (showDistance) {
+ setSelectedtime(val);
+ } else {
+ opentoast('Select Pickup and Drop', 'error');
+ }
+ }}
+ sx={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: 0.5,
+ px: 1.25,
+ py: 0.5,
+ cursor: 'pointer',
+ borderRadius: 999,
+ border: `1.5px solid ${active ? '#f59e0b' : edge('#f59e0b')}`,
+ bgcolor: active ? '#f59e0b' : tint('#f59e0b'),
+ color: active ? '#fff' : '#f59e0b',
+ fontSize: 12,
+ fontWeight: 800,
+ transition: 'all 0.15s',
+ boxShadow: active ? `0 6px 18px ${ring('#f59e0b')}` : 'none',
+ '&:hover': {
+ borderColor: '#f59e0b',
+ boxShadow: active ? `0 6px 18px ${ring('#f59e0b')}` : `0 0 0 3px ${ring('#f59e0b')}`
+ }
+ }}
+ >
+ {dayjs(val).format('hh:mm A')}
+
+ );
+ }
+ })}
+
+
+
-
-
+
+
-
- {showDistance && (
-
-
-
-
-
-
+
+ }
+ title="Distance & Pricing"
+ subtitle="Auto-calculated from pickup → drop"
+ />
+
+ {showDistance && (
+
+
+
+
+
+
+
+ )}
+
+ Category
+ `${option.subcategoryname}` || ''}
+ sx={{ my: 2, zIndex: '100' }}
+ fullWidth
+ renderInput={(params) => }
+ onChange={(event, value, reason) => {
+ if (value) {
+ console.log(value);
+ setSubCatName(value.subcategoryname);
+ setSubCatId(value.subcategoryid);
+ }
+ }}
+ />
- )}
-
- Category
- `${option.subcategoryname}` || ''}
- sx={{ my: 2, zIndex: '100' }}
- fullWidth
- renderInput={(params) => }
- onChange={(event, value, reason) => {
- if (value) {
- console.log(value);
- setSubCatName(value.subcategoryname);
- setSubCatId(value.subcategoryid);
- }
- }}
- />
-
- Weight
-
- {
- handleChipClick('1-10kgs');
- setWeight('1-10kgs');
- }}
- />
- {
- handleChipClick('11-20kgs');
- setWeight('11-20kgs');
- }}
- />
- {
- handleChipClick('21-30kgs');
- setWeight('21-30kgs');
- }}
- />
-
-
- SMS Delivery
- {
- setIsSms(e.target.checked ? 1 : 0);
- }}
- />
-
-
+ Weight
+
+ {
+ handleChipClick('1-10kgs');
+ setWeight('1-10kgs');
+ }}
+ />
+ {
+ handleChipClick('11-20kgs');
+ setWeight('11-20kgs');
+ }}
+ />
+ {
+ handleChipClick('21-30kgs');
+ setWeight('21-30kgs');
+ }}
+ />
+
+
+ SMS Delivery
+ {
+ setIsSms(e.target.checked ? 1 : 0);
+ }}
+ />
+
+
+
-
-
-
- {
- setCollectionamt(e.target.value);
- }}
- inputProps={{ min: 0 }}
- />
-
-
-
-
- {
- setQuantity(e.target.value);
- }}
- inputProps={{ min: 1 }}
- />
-
-
-
+
+ {/* ============================== || Cash Collect & Quantity || ============================== */}
+
+ }
+ title="Collection & Quantity"
+ subtitle="Cash to collect on delivery, total items"
+ />
+
+
+
+ setCollectionamt(e.target.value)}
+ inputProps={{ min: 0 }}
+ />
+
+
+ setQuantity(e.target.value)}
+ inputProps={{ min: 1 }}
+ />
+
+
+
+
{/* ================================================= || Notes || ================================================= */}
-
-
-
- setOtherinstructions(e.target.value)}
- />
-
- {/* */}
-
-
+
+ }
+ title="Notes"
+ subtitle="Add anything the rider should know"
+ />
+
+ setOtherinstructions(e.target.value)}
+ />
+
+
+ {!showDistance && (
+
+ Set pickup & drop to enable
+
+ )}
}
onClick={() => {
setLoading(true);
setBtnLoading(true);
@@ -2302,17 +2569,33 @@ const Createorder1 = () => {
}, 1000);
}}
sx={{
+ borderRadius: 999,
+ px: 3,
+ py: 1,
+ fontWeight: 800,
+ textTransform: 'none',
+ fontSize: 13,
+ background: `linear-gradient(135deg, ${BRAND} 0%, ${BRAND_LIGHT} 100%)`,
+ color: '#fff',
+ boxShadow: `0 8px 22px ${ring(BRAND)}`,
+ transition: 'all 0.18s',
'&:hover': {
- transform: 'scale(1.05)',
- transition: 'transform 0.3s ease'
+ background: `linear-gradient(135deg, #4D1C61 0%, ${BRAND} 100%)`,
+ transform: 'translateY(-1px)',
+ boxShadow: `0 10px 26px ${ring(BRAND)}`
+ },
+ '&.Mui-disabled': {
+ background: DT.divider,
+ color: DT.textMuted,
+ boxShadow: 'none'
}
}}
>
- {btnLoading ? : 'Create'}
+ {btnLoading ? : 'Create Order'}
-
-
+
+
@@ -2371,176 +2654,243 @@ const Createorder1 = () => {
{/* ============================================= || saved address Dialog || ============================================= */}
-
>
diff --git a/src/pages/nearle/orders/details.js b/src/pages/nearle/orders/details.js
index 4ec47a3..f1e2376 100644
--- a/src/pages/nearle/orders/details.js
+++ b/src/pages/nearle/orders/details.js
@@ -97,7 +97,91 @@ import {
// DeleteTwoTone
} from '@ant-design/icons';
import { enqueueSnackbar } from 'notistack';
+import {
+ MdLocalShipping,
+ MdHourglassEmpty,
+ MdCheckCircle,
+ MdCancel,
+ MdAccessTime,
+ MdHistoryToggleOff,
+ MdAssignmentTurnedIn,
+ MdEdit,
+ MdArrowBack,
+ MdReceiptLong
+} from 'react-icons/md';
+import { Paper, Box } from '@mui/material';
+// ============================================================================
+// Design tokens — shared with the rest of the redesigned operator pages.
+// ============================================================================
+const DT = {
+ radiusPill: 999,
+ radiusCard: 16,
+ shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
+ shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
+ shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
+ textPrimary: '#0f172a',
+ textSecondary: '#64748b',
+ textMuted: '#94a3b8',
+ borderSubtle: '#e2e8f0',
+ divider: '#f1f5f9',
+ surface: '#ffffff',
+ surfaceAlt: '#f8fafc'
+};
+const dtA = (c, suffix) => `${c}${suffix}`;
+const tint = (c) => dtA(c, '08');
+const soft = (c) => dtA(c, '18');
+const ring = (c) => dtA(c, '26');
+const edge = (c) => dtA(c, '55');
+
+const BRAND = '#662582';
+const BRAND_LIGHT = '#9255AB';
+
+// Semantic per-status palette — also drives StatusBadge.
+const STATUS_META = {
+ pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
+ assigned: { label: 'Assigned', color: '#0ea5e9', icon: MdAssignmentTurnedIn },
+ confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
+ modified: { label: 'Modified', color: '#06b6d4', icon: MdHistoryToggleOff },
+ processing: { label: 'Processing', color: BRAND, icon: MdAccessTime },
+ active: { label: 'Active', color: '#8b5cf6', icon: MdLocalShipping },
+ closed: { label: 'Closed', color: '#06b6d4', icon: MdCheckCircle },
+ completed: { label: 'Completed', color: '#10b981', icon: MdCheckCircle },
+ cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
+};
+
+const StatusBadge = ({ status, size = 'md' }) => {
+ if (!status) return null;
+ const meta = STATUS_META[String(status).toLowerCase()] || {
+ label: status,
+ color: DT.textMuted,
+ icon: MdHistoryToggleOff
+ };
+ const Icon = meta.icon;
+ const px = size === 'lg' ? 1.25 : 1;
+ const py = size === 'lg' ? 0.5 : 0.375;
+ const fs = size === 'lg' ? 12 : 11;
+ return (
+
+ {meta.label}
+
+ );
+};
const Details = () => {
// const [searchParams] = useSearchParams();
@@ -854,59 +938,105 @@ const Details = () => {
open={open}
onClose={() => handleClose(false)}
maxWidth="xs"
-
+ PaperProps={{ sx: { borderRadius: 3 } }}
>
-
-
-
+
+
+
-
-
-
-
- {/*
- Are you sure you want to cancel this order?
- */}
- {(invoiceeligible) &&
-
-
- }>
- Order is within 24Hrs time frame. The order will be invoiced with standard pricing as agreed.
- {/* This is an warning alert. */}
- Terms & Condition link
-
- }
-
- Please type in the order number to confirm.
+
+
+ Cancel Order
+
+
+ Confirm to permanently cancel this order
- {
- console.log(e.target.value)
- setDeletepassword(e.target.value)
- }}
- error={deletepassword !== orderid.slice(4)}
- // error={true}
- value={deletepassword}
- />
-
+
+
+
+
+
+ {orderid.slice(4)}
+
-
- {
- if (deletepassword === orderid.slice(4)) {
- cancelorder();
- handleClose(true);
- }
+ {(invoiceeligible) &&
+ }>
+ Order is within 24Hrs time frame. The order will be invoiced with standard pricing as agreed.
+ Terms & Condition link
+
+ }
- }} autoFocus>
- Yes, Cancel
-
- handleClose(false)} color="secondary" variant="outlined">
+
+ Please type in the order number to confirm.
+
+ setDeletepassword(e.target.value)}
+ error={deletepassword !== orderid.slice(4)}
+ value={deletepassword}
+ placeholder={orderid.slice(4)}
+ />
+
+
+ handleClose(false)}
+ variant="outlined"
+ sx={{
+ borderRadius: 999,
+ py: 1,
+ borderColor: DT.borderSubtle,
+ color: DT.textSecondary,
+ fontWeight: 700,
+ '&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
+ }}
+ >
No
-
+ {
+ if (deletepassword === orderid.slice(4)) {
+ cancelorder();
+ handleClose(true);
+ }
+ }}
+ sx={{
+ borderRadius: 999,
+ py: 1,
+ bgcolor: '#ef4444',
+ fontWeight: 700,
+ boxShadow: `0 6px 18px ${ring('#ef4444')}`,
+ '&:hover': { bgcolor: '#dc2626' }
+ }}
+ >
+ Yes, Cancel
+
@@ -925,40 +1055,116 @@ const Details = () => {
// fullScreen
TransitionComponent={PopupTransition}>
-
-
-
- Assign Roles
-
-
+
+
+
+
+
+
+
+
+ Assign Roles
+
+
+ {clientname} · {currentrole}
+
+
-
-
+
+ {orderid}
+
-
+
-
- {/* */}
- setTabstatus((e) => (e === 0) ? 1 : 0)}
- variant="scrollable" scrollButtons="auto" >
- {/* */}
-
-
+ setTabstatus((e) => (e === 0 ? 1 : 0))}
+ sx={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: 0.75,
+ px: 1.25,
+ py: 0.625,
+ borderRadius: 999,
+ cursor: 'pointer',
+ bgcolor: BRAND,
+ color: '#fff',
+ fontWeight: 800,
+ fontSize: 12,
+ boxShadow: `0 6px 18px ${ring(BRAND)}`
+ }}
+ >
+
+
+
+ {currentrole || 'Role'}
+
-
-
-
-
-
+
+ {[
+ { label: 'Required', value: currentshiftobj.shifts, color: BRAND },
+ { label: 'Assigned', value: currentshiftobj.assigned, color: '#10b981' },
+ { label: 'Remaining', value: currentshiftobj.remaining, color: '#ef4444' }
+ ].map((c) => (
+
+ {c.label}
+
+ {c.value ?? 0}
+
+
+ ))}
-
@@ -1256,215 +1462,194 @@ const Details = () => {
Details
*/}
-
-
-
-
-
+
+ history.back()}
+ sx={{
+ bgcolor: '#fff',
+ border: `1px solid ${DT.borderSubtle}`,
+ borderRadius: 999,
+ color: DT.textPrimary,
+ '&:hover': { bgcolor: tint(BRAND), borderColor: edge(BRAND), color: BRAND }
+ }}
>
-
- history.back()}
- // onClick={()=>}
+
+
+
+
+
+
+
-
-
- {/* Test me */}
-
- Details
-
- {/* */}
- : orderid} variant="combined" color='warning' size='small' />
- {/* Date */}
- {/* {orderdate} */}
- : orderdate} variant="combined" color="primary" size='small' />
-
-
-
- {(orderstatus === 'pending') &&
-
-
- }
- {(orderstatus === 'cancelled') &&
-
-
-
- }
- {(orderstatus === 'completed') &&
-
-
- }
- {(orderstatus === 'processing') &&
-
- }
- {(orderstatus === 'assigned') &&
-
- }
- {(orderstatus === 'confirmed') &&
-
- }
-
- {(orderstatus === 'active') &&
-
- }
- {(orderstatus === 'closed') &&
-
- }
-
- {(orderstatus === 'modified') &&
-
- }
-
-
-
-
-
-
-
-
- {/* {dayjs(startdate).$d.toString()} */}
- {/* {startdate} */}
- {/* {dayjs().$d.toString()} */}
-
- {(((orderstatus === 'pending')
- || (orderstatus === 'assigned')
- || (orderstatus === 'confirmed')
- || (orderstatus === 'modified'))
-
-
- // && (dayjs(startdate).$d > dayjs().$d)
- ) &&
-
-
-
-
- }
- onClick={(e) => {
- e.stopPropagation();
- // if (dayjs(startdate).$d > dayjs().$d) {
- if (dayjs(dayjs().format('MM-DD-YYYY')).isBefore(dayjs(dayjs(startdate).format('MM-DD-YYYY')))) {
- navigate(`/editorder`
- , {
- state: {
- orderheaderid: orderheaderid,
- tenantid: tenantid
- }
- }
- )
- } else {
- enqueueSnackbar('Order cannot be edited.\n Order date is not valid at this time',
-
- {
-
- variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: 4000,
- style: { whiteSpace: "pre-line" }
- })
-
- }
-
- }}
- >
- Edit Order
-
-
- }
-
-
-
-
- {/* {(((orderstatus === 'pending')
- || (orderstatus === 'modified'))
- && assignedpendingcount === 0) &&
- <>
- }
- onClick={() => {
- fetchassignedstaffs();
- }}
- >
- Notify Staff
-
- >
- } */}
- {(orderstatus !== 'cancelled' && orderstatus !== '' && orderstatus !== 'completed' && orderstatus !== 'closed') &&
- <>
- {
- console.log(dayjs(startdate).diff(dayjs(), 'm') / 60)
- if ((dayjs(startdate).diff(dayjs(), 'm') / 60) > 24) {
- setInvoiceeligible(false)
- setOpen(true)
- } else {
- setInvoiceeligible(true)
- setOpen(true)
-
- }
-
- }}
- sx={{ borderRadius: '40px', mt: { xs: 2, sm: 0 } }}
- startIcon={}
- >
- Cancel Order
-
- >
- }
- {(orderstatus === 'cancelled') &&
- <>
-
- >
- }
- {/* {(orderstatus === 'completed') &&
- {
- navigate(`/invoice/create`, {
- state: {
- orderheaderid: orderheaderid,
- tenantid: tenantid
- }
- })
-
- }
- }
- sx={{ borderRadius: '40px', mt: { xs: 2, sm: 0 } }}
+ Order Details
+
+
+
- Raise Invoice
-
- } */}
+
+ {orderid === '' ? : orderid}
+
+
+
+ {orderdate === '' ? : orderdate}
+
+
+
-
-
-
-
+
+
+ {((orderstatus === 'pending') ||
+ (orderstatus === 'assigned') ||
+ (orderstatus === 'confirmed') ||
+ (orderstatus === 'modified')) && (
+
+ }
+ onClick={(e) => {
+ e.stopPropagation();
+ if (dayjs(dayjs().format('MM-DD-YYYY')).isBefore(dayjs(dayjs(startdate).format('MM-DD-YYYY')))) {
+ navigate(`/editorder`, {
+ state: {
+ orderheaderid: orderheaderid,
+ tenantid: tenantid
+ }
+ });
+ } else {
+ enqueueSnackbar('Order cannot be edited.\n Order date is not valid at this time', {
+ variant: 'error',
+ anchorOrigin: { vertical: 'top', horizontal: 'right' },
+ autoHideDuration: 4000,
+ style: { whiteSpace: 'pre-line' }
+ });
+ }
+ }}
+ >
+ Edit Order
+
+
+ )}
+
+ {orderstatus !== 'cancelled' && orderstatus !== '' && orderstatus !== 'completed' && orderstatus !== 'closed' && (
+ {
+ if ((dayjs(startdate).diff(dayjs(), 'm') / 60) > 24) {
+ setInvoiceeligible(false);
+ setOpen(true);
+ } else {
+ setInvoiceeligible(true);
+ setOpen(true);
+ }
+ }}
+ startIcon={}
+ >
+ Cancel Order
+
+ )}
+
+ {orderstatus === 'cancelled' && (
+
+ Cancelled on {cancelleddate}
+
+ )}
+
+
+
{/* Dialog window */}
diff --git a/src/pages/nearle/orders/orders.js b/src/pages/nearle/orders/orders.js
index 7691fa1..ee96263 100644
--- a/src/pages/nearle/orders/orders.js
+++ b/src/pages/nearle/orders/orders.js
@@ -1,26 +1,20 @@
import * as React from 'react';
import { enqueueSnackbar } from 'notistack';
-import { DeleteFilled } from '@ant-design/icons';
import { useState, useEffect, Fragment, useRef } from 'react';
-import { Empty } from 'antd';
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import axios from 'axios';
-import HoverSocialCard from 'components/cards/statistics/HoverSocialCard';
import { useTheme } from '@mui/material/styles';
-import MyLocationIcon from '@mui/icons-material/MyLocation';
-import { CalendarMonth, CancelOutlined, CheckCircleOutline } from '@mui/icons-material';
import {
Avatar,
+ Box,
Button,
Grid,
- Tabs,
- Tab,
IconButton,
+ Paper,
Stack,
- Chip,
Typography,
Table,
TableCell,
@@ -30,36 +24,200 @@ import {
TableRow,
DialogContent,
Tooltip,
- FormControl,
- OutlinedInput,
- InputAdornment,
Skeleton,
Autocomplete,
TextField,
- CircularProgress
+ CircularProgress,
+ InputBase,
+ InputAdornment
} from '@mui/material';
-import { SearchOutlined, CloseOutlined } from '@ant-design/icons';
-import ClearIcon from '@mui/icons-material/Clear';
+import {
+ MdAccessTime,
+ MdCancel,
+ MdCheckCircle,
+ MdClose,
+ MdCurrencyRupee,
+ MdDeleteOutline,
+ MdHistoryToggleOff,
+ MdHourglassEmpty,
+ MdInventory2,
+ MdLocalShipping,
+ MdLocationOn,
+ MdMyLocation,
+ MdNote,
+ MdPlace,
+ MdSearch,
+ MdStraighten,
+ MdCalendarMonth,
+ MdReceiptLong,
+ MdClear
+} from 'react-icons/md';
import TableContainer from '@mui/material/TableContainer';
-import TablePagination from '@mui/material/TablePagination';
import Loader from 'components/Loader';
-// import { FilterList } from '@mui/icons-material';
import { useHotkeyFocus } from 'components/nearle_components/useHotkeyFocus';
import DateFilterDialog from 'components/nearle_components/DateFilterDialog';
-import MainCard from 'components/MainCard';
-import AccessTimeIcon from '@mui/icons-material/AccessTime';
-import LocalShippingOutlinedIcon from '@mui/icons-material/LocalShippingOutlined';
import CircularLoader from 'components/nearle_components/CircularLoader';
import { useInfiniteQuery } from '@tanstack/react-query';
+// ============================================================================
+// Design tokens — shared with the rest of the redesigned operator pages.
+// ============================================================================
+const DT = {
+ radiusPill: 999,
+ radiusCard: 16,
+ shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
+ shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
+ shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
+ textPrimary: '#0f172a',
+ textSecondary: '#64748b',
+ textMuted: '#94a3b8',
+ borderSubtle: '#e2e8f0',
+ divider: '#f1f5f9',
+ surface: '#ffffff',
+ surfaceAlt: '#f8fafc'
+};
+const dtA = (c, suffix) => `${c}${suffix}`;
+const tint = (c) => dtA(c, '08');
+const soft = (c) => dtA(c, '18');
+const ring = (c) => dtA(c, '26');
+const edge = (c) => dtA(c, '55');
+
+const BRAND = '#662582';
+const BRAND_LIGHT = '#9255AB';
+
+const SoftPaper = (props) => (
+
+);
+
+const AccentAvatar = ({ color, selected, size = 24, children }) => (
+
+ {children}
+
+);
+
+const pillFieldSx = (color) => ({
+ '& .MuiOutlinedInput-root': {
+ borderRadius: DT.radiusPill + 'px',
+ bgcolor: tint(color),
+ fontWeight: 600,
+ '& fieldset': { borderColor: edge(color), borderWidth: 1.5 },
+ '&:hover fieldset': { borderColor: color },
+ '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
+ '&.Mui-focused fieldset': { borderColor: color, borderWidth: 2 }
+ }
+});
+
+// Semantic per-row status palette.
+const ROW_STATUS_META = {
+ created: { label: 'Created', color: '#0ea5e9', icon: MdLocalShipping },
+ pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
+ processing: { label: 'Processing', color: BRAND, icon: MdAccessTime },
+ modified: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
+ confirmed: { label: 'Confirmed', color: '#10b981', icon: MdCheckCircle },
+ ready: { label: 'Accepted', color: '#6366f1', icon: MdCheckCircle },
+ active: { label: 'Picked', color: '#8b5cf6', icon: MdLocalShipping },
+ closed: { label: 'Closed', color: '#06b6d4', icon: MdCheckCircle },
+ delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
+ cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
+};
+
+// Top-level pill tabs.
+const ORDERS_STATUS_TABS = [
+ { idx: 0, status: 'created', label: 'Created', color: BRAND, icon: MdLocalShipping, countKey: 'created' },
+ { idx: 1, status: 'pending', label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, countKey: 'pending' },
+ { idx: 2, status: 'delivered', label: 'Delivered', color: '#10b981', icon: MdCheckCircle, countKey: 'delivered' },
+ { idx: 3, status: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdCancel, countKey: 'cancelled' }
+];
+
+const StatusBadge = ({ status }) => {
+ const meta = ROW_STATUS_META[String(status || '').toLowerCase()] || {
+ label: status || '—',
+ color: DT.textMuted,
+ icon: MdHistoryToggleOff
+ };
+ const Icon = meta.icon;
+ return (
+
+ {meta.label}
+
+ );
+};
+
+const MetricCell = ({ value, color, icon, isMoney = false }) => {
+ const n = Number(value);
+ const display = isMoney ? `₹${Number.isFinite(n) ? n.toFixed(2) : '0.00'}` : Number.isFinite(n) ? n : value || 0;
+ const isZero = !Number.isFinite(n) || n === 0;
+ if (isZero) {
+ return (
+
+ {display}
+
+ );
+ }
+ return (
+
+ {icon}
+ {display}
+
+ );
+};
+
const Orders = () => {
const tid = localStorage.getItem('tenantid');
const tenId = localStorage.getItem('tenantid');
const loadMoreRef = useRef();
const containerRef = useRef();
- const [page, setPage] = React.useState(0);
- const [rowsPerPage, setRowsPerPage] = React.useState(10);
- const [pageCount, setPageCount] = React.useState(0);
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(10);
+ const [pageCount, setPageCount] = useState(0);
const [percentage1, setPercentage1] = useState('0');
const [percentage2, setPercentage2] = useState('0');
const [percentage3, setPercentage3] = useState('0');
@@ -81,74 +239,41 @@ const Orders = () => {
const [cancelOpen, setCancelOpen] = useState(false);
const [orderheaderid, setOrderheaderid] = useState('');
const [locationId, setLocationId] = useState(0);
- const [locoName, setLocoName] = useState('Select Location');
+ const [locoName, setLocoName] = useState('All Locations');
const [dateOpen, setDateOpen] = useState(false);
const [datestatus, setDatestatus] = useState('Today');
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
-
const [searchword, setSearchword] = useState('');
const [debouncedSearch, setDebouncedSearch] = useState('');
- const getValueColor = (value) => {
- return value !== 0 ? 'red' : 'inherit';
- };
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearch(searchword);
}, 400);
-
return () => clearTimeout(handler);
}, [searchword]);
- const statusMap = [
- {
- label: 'Created',
- value: 'created',
- count: createdLenght,
- icon:
- },
- {
- label: 'Pending',
- value: 'pending',
- count: pendingLenght,
- icon:
- },
- {
- label: 'Delivered',
- value: 'delivered',
- count: deliveredlenght,
- icon:
- },
- {
- label: 'Cancelled',
- value: 'cancelled',
- count: cancelledLenght,
- icon:
- }
- ];
+ const tabCounts = {
+ created: createdLenght,
+ pending: pendingLenght,
+ delivered: deliveredlenght,
+ cancelled: cancelledLenght
+ };
const handleChangetab = (e, i) => {
setSearchword('');
setRowsPerPage(10);
setTabvalue(i);
- setTabstatus(statusMap[i].label);
- setCurrentStatus(statusMap[i].value);
+ const tab = ORDERS_STATUS_TABS[i];
+ setTabstatus(tab.label);
+ setCurrentStatus(tab.status);
setPage(0);
};
const textFieldRef = useRef(null);
useHotkeyFocus(textFieldRef, 'k');
- const handleChangePage = (event, newPage) => {
- setPage(newPage);
- };
-
- const handleChangeRowsPerPage = (event) => {
- setRowsPerPage(parseInt(event.target.value, 10));
- setPage(0);
- };
-
const cancelorder = async () => {
setLoading(true);
await axios
@@ -158,7 +283,6 @@ const Orders = () => {
cancelled: dayjs().format('YYYY-MM-DD HH:mm:ss')
})
.then((res) => {
- console.log(res);
if (res.data.status) {
enqueueSnackbar('Order Cancelled Successfully', {
variant: 'success',
@@ -177,34 +301,10 @@ const Orders = () => {
});
};
- // const getOrders = async () => {
- // try {
- // await axios
- // .get(
- // `${
- // process.env.REACT_APP_URL
- // }/orders/tenant/getorders/?tenantid=${tid}&locationid=${locationId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${
- // page + 1
- // }&pagesize=${rowsPerPage}&keyword=${debouncedSearch}`
- // )
- // .then((res) => {
- // setRows(res.data.details);
- // })
- // .catch((err) => {
- // console.log(err);
- // });
- // } catch (err) {
- // console.log(err);
- // }
- // };
- // useEffect(() => {
- // getOrders();
- // }, [tabstatus, startdate, enddate, page, rowsPerPage, debouncedSearch, locationId]);
const fetchOrders = async ({ pageParam = 1 }) => {
const res = await axios.get(
`${process.env.REACT_APP_URL}/orders/tenant/getorders/?tenantid=${tid}&locationid=${locationId}&status=${currentStatus}&fromdate=${startdate}&todate=${enddate}&pageno=${pageParam}&pagesize=${rowsPerPage}&keyword=${debouncedSearch}`
);
-
return {
data: res.data.details,
nextPage: res.data.details.length === rowsPerPage ? pageParam + 1 : undefined
@@ -213,14 +313,11 @@ const Orders = () => {
const {
data: rowdata,
- isError: isErrorGetOrders,
- error: errorGetOrders,
fetchNextPage,
- isLoading: isLoadingGeyOrders,
+ isLoading: isLoadingGetOrders,
hasNextPage,
isFetchingNextPage,
refetch: refetchOrders
- // status: rowdataStatus
} = useInfiniteQuery({
queryKey: [tabstatus, startdate, enddate, page, rowsPerPage, debouncedSearch, locationId],
queryFn: fetchOrders,
@@ -238,7 +335,7 @@ const Orders = () => {
}
},
{
- root: document.querySelector('.MuiTableContainer-root'), // 👈 or explicitly TableContainer
+ root: document.querySelector('.MuiTableContainer-root'),
rootMargin: '0px',
threshold: 1.0
}
@@ -264,7 +361,6 @@ const Orders = () => {
await axios
.get(`${process.env.REACT_APP_URL}/orders/getordersummary/?tenantid=${tid}`)
.then((res) => {
- console.log('summary', res.data.details);
setCoveredorders(res.data.details.delivered.toString());
setCancelled(res.data.details.cancelled.toString());
setUncoveredorders(res.data.details.pending.toString());
@@ -296,7 +392,6 @@ const Orders = () => {
`${process.env.REACT_APP_URL}/orders/getordersummary/?tenantid=${tid}&locationid=${locationId}&fromdate=${startdate}&todate=${enddate}`
)
.then((res) => {
- console.log('fetchorderscount', res.data.details);
setCreatedLenght(res.data.details.created);
setPendingLenght(res.data.details.pending);
setDeliveredlenght(res.data.details.delivered);
@@ -319,18 +414,12 @@ const Orders = () => {
useEffect(() => {
fetchorderscount();
}, [tabvalue, locationId, startdate, enddate]);
+
// ============================================= || gettenantlocations (branches) || =============================================
const gettenantlocations = async (id) => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
- console.log('gettenantlocations', res.data.details);
- if (res.data.details.length == 1) {
- setIsLocation(true);
- setTenantlocations(res.data.details);
- setPickCust(res.data.details[0]);
- } else {
- setTenantlocations(res.data.details);
- }
+ setTenantlocations(res.data.details || []);
} catch (err) {
console.log('gettenantlocations', err);
}
@@ -339,500 +428,794 @@ const Orders = () => {
gettenantlocations(tenId);
}, []);
+ // KPI tile definitions.
+ const kpiCards = [
+ { key: 'created', label: 'Created Orders', color: BRAND, icon: MdLocalShipping, value: created, percentage: percentage1 },
+ { key: 'pending', label: 'Pending Orders', color: '#f59e0b', icon: MdHourglassEmpty, value: uncoveredorders, percentage: percentage2 },
+ { key: 'delivered', label: 'Delivered Orders', color: '#10b981', icon: MdCheckCircle, value: coveredorders, percentage: percentage3 },
+ { key: 'cancelled', label: 'Cancelled Orders', color: '#ef4444', icon: MdCancel, value: cancelled, percentage: percentage4 }
+ ];
+
return (
- <>
+
{loading && (
<>
>
)}
- {/* ============================================== || TitleCard || ============================================== */}
-
- Orders
-
- {/* ============================================== || HoverSocialCard || ============================================== */}
-
-
- : created}
- percentage={percentage1.toString()}
- color={theme.palette.primary.main}
- />
-
-
- : uncoveredorders}
- percentage={percentage2.toString()}
- color={theme.palette.warning.main}
- />
-
-
-
- : coveredorders}
- percentage={percentage3.toString()}
- color={theme.palette.success.main}
- />
-
-
-
- : cancelled}
- percentage={percentage4.toString()}
- color={theme.palette.secondary[600]}
- />
-
-
- {/* ============================================== || Filters || ============================================== */}
-
-
- {startdate && enddate && (
-
-
-
-
- {dayjs(startdate).format('DD/MM/YYYY')} - {dayjs(enddate).format('DD/MM/YYYY')}
-
- }
- sx={{ cursor: 'pointer' }}
- variant="combined"
- color="warning"
- onClick={() => setDateOpen(true)}
- deleteIcon={}
- onDelete={() => setDateOpen(true)}
- />
-
- )}
- {tenantLocations.length == 1 ? (
-
-
-
- )
+
+
+ >
+
+
+
+
+ Orders
+
+
+
+
+ Live · {locoName} · {datestatus}
+
+
+
+
+
+ {/* Location picker */}
+ {tenantLocations.length === 1 ? (
+
+ {tenantLocations[0].locationname}
+
) : (
`${option.locationname} (${option.suburb})` || ''}
- renderInput={(params) => }
- sx={{
- width: { xs: '100%', custom800: 400 }
- }}
+ getOptionLabel={(option) => (option ? `${option.locationname} (${option.suburb || ''})` : '')}
+ PaperComponent={SoftPaper}
onChange={(event, value, reason) => {
if (value) {
- console.log('Business Locations', value);
setLocationId(value.locationid);
setLocoName(value.locationname);
}
- if (reason == 'clear') {
+ if (reason === 'clear') {
setLocationId(0);
- setLocoName('Select Location');
+ setLocoName('All Locations');
}
}}
+ renderInput={(params) => (
+
+
+
+
+
+ )
+ }}
+ />
+ )}
+ sx={{ width: { xs: '100%', sm: 280 } }}
/>
)}
- {/*
- setDateOpen(true)}
- >
-
-
- */}
-
- {/* ============================================== || Table || ============================================== */}
-
- {/* Tabs Wrapper */}
+
-
- {statusMap.map((item, index) => (
-
- {item.icon}
- {item.label}
-
-
- }
- />
- ))}
-
-
- {/* Search Box */}
-
-
- setSearchword(e.target.value)}
- autoComplete="off"
- startAdornment={
-
-
-
- }
- endAdornment={
-
- {
- setSearchword('');
- refetchOrders();
- fetchorderscount();
+ {/* ============================================= || KPI Cards || ============================================= */}
+
+ {kpiCards.map((item) => {
+ const Icon = item.icon;
+ return (
+
+
+
+
+
+
+ {item.label}
+
+
+ {item.value === '' ? : item.value}
+
+ {item.percentage != null && (
+
+ {item.percentage}%
+
+ )}
+
+
-
-
-
- }
+
+
+
+
+
+ );
+ })}
+
+
+ {/* ============================================= || Filter Bar || ============================================= */}
+
+
+
+
+ setDateOpen(true)}
+ sx={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: 0.75,
+ px: 1.25,
+ py: 0.75,
+ borderRadius: 999,
+ cursor: 'pointer',
+ bgcolor: tint('#f59e0b'),
+ border: `1.5px solid ${edge('#f59e0b')}`,
+ color: '#f59e0b',
+ fontWeight: 800,
+ fontSize: 12,
+ transition: 'all 0.18s',
+ '&:hover': { borderColor: '#f59e0b', boxShadow: `0 0 0 3px ${ring('#f59e0b')}` }
+ }}
+ >
+
+ {dayjs(startdate).format('DD/MM/YY')} – {dayjs(enddate).format('DD/MM/YY')}
+
+
+
+ {datestatus}
+
+
+
+ {tenantLocations.length > 1 && (
+ (option ? `${option.locationname} (${option.suburb || ''})` : '')}
+ PaperComponent={SoftPaper}
+ onChange={(event, value, reason) => {
+ if (value) {
+ setLocationId(value.locationid);
+ setLocoName(value.locationname);
+ }
+ if (reason === 'clear') {
+ setLocationId(0);
+ setLocoName('All Locations');
+ }
+ }}
+ renderInput={(params) => (
+
+
+
+
+
+ )
+ }}
+ />
+ )}
+ sx={{ width: { xs: '100%', md: 320 } }}
/>
-
+ )}
-
-
-
-
+
+ {/* ============================================= || Status Tabs + Search || ============================================= */}
+
+
+
-
-
-
- S.No
-
- {' '}
- Orders Location
+ {ORDERS_STATUS_TABS.map((t) => {
+ const Icon = t.icon;
+ const active = tabvalue === t.idx;
+ const count = tabCounts[t.countKey] ?? 0;
+ return (
+ handleChangetab(e, t.idx)}
+ sx={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: { xs: 0.625, md: 0.875 },
+ pl: 0.5,
+ pr: { xs: 1, md: 1.25 },
+ py: 0.5,
+ flexShrink: 0,
+ cursor: 'pointer',
+ borderRadius: 999,
+ border: `1.5px solid ${active ? t.color : edge(t.color)}`,
+ bgcolor: active ? t.color : tint(t.color),
+ color: active ? '#fff' : t.color,
+ fontWeight: 700,
+ boxShadow: active ? `0 6px 18px ${ring(t.color)}` : 'none',
+ transition: 'all 0.18s',
+ '&:hover': {
+ borderColor: t.color,
+ boxShadow: active ? `0 6px 18px ${ring(t.color)}` : `0 0 0 3px ${ring(t.color)}`
+ }
+ }}
+ >
+
+
+
+
+ {t.label}
+
+
+ {count ?? 0}
+
+
+ );
+ })}
+
+
+
+
+
+ setSearchword(e.target.value)}
+ autoComplete="off"
+ sx={{
+ flex: 1,
+ fontSize: 13,
+ fontWeight: 600,
+ color: DT.textPrimary,
+ '& input::placeholder': { color: DT.textMuted, opacity: 1 }
+ }}
+ />
+ {searchword && (
+ {
+ setSearchword('');
+ refetchOrders();
+ fetchorderscount();
+ }}
+ sx={{ p: 0.25, color: BRAND }}
+ >
+
+
+ )}
+
+
+
+
+
+ {/* ============================================= || Table || ============================================= */}
+
+
+
+
+
+ #
+ Order Location
+ Pickup
+ Drop
+ Qty
+ COD
+ Kms
+ Charges
+ Notes
+ Status
+ {currentStatus === 'created' && Actions}
+
+
+
+
+ {(isLoadingGetOrders || loading) &&
+ rows.length === 0 &&
+ [0, 1, 2, 3, 4, 5].map((_, idx) => (
+
+ {Array.from({ length: currentStatus === 'created' ? 11 : 10 }).map((__, ci) => (
+
+
+
+ ))}
+
+ ))}
+
+ {!isLoadingGetOrders && rows.length === 0 && (
+
+
+
+
+
+
+
+ No {currentStatus} orders
+
+
+ {searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'}
+
+
- Pickup
- Drop
- Qty
- Cod
- kms
- Charges
- Notes
- Status
- {currentStatus == 'created' && (
- Action
+
+ )}
+
+ {rows.map((row, index) => (
+
+
+
+ {page * rowsPerPage + index + 1}
+
+
+
+
+
+ {row.locationname}
+ {row.locationsuburb && ` - ${row.locationsuburb}`}
+
+
+
+ {row.orderid}
+
+
+
+
+
+ {dayjs(row.pickupslot).format('DD/MM/YYYY')} · {dayjs(row.pickupslot).format('hh:mm A')}
+
+
+
+
+
+
+
+ {row.pickupcustomer}
+
+
+ {row.pickupcontactno}
+
+
+
+ {row.pickupsuburb || (row.pickupaddress ? `${row.pickupaddress.slice(0, 20)}…` : '—')}
+
+
+
+
+
+
+
+
+ {row.deliverycustomer}
+
+
+ {row.deliverycontactno}
+
+
+
+ {row.deliverysuburb ||
+ (row.deliveryaddress?.length > 20 ? `${row.deliveryaddress.slice(0, 20)}…` : row.deliveryaddress || '—')}
+
+
+
+
+
+
+ } />
+
+
+
+ } isMoney />
+
+
+
+ } />
+
+
+
+ } isMoney />
+
+
+
+ {row.ordernotes ? (
+
+
+
+ {row.ordernotes}
+
+
+ ) : (
+
+ —
+
+ )}
+
+
+
+
+
+
+ {currentStatus === 'created' && (
+
+ {row.orderstatus === 'created' && (
+
+ {
+ e.stopPropagation();
+ setOrderheaderid(row.orderheaderid);
+ setCancelOpen(true);
+ }}
+ sx={{
+ bgcolor: tint('#ef4444'),
+ border: `1px solid ${edge('#ef4444')}`,
+ color: '#ef4444',
+ borderRadius: 999,
+ p: 0.75,
+ '&:hover': {
+ bgcolor: soft('#ef4444'),
+ borderColor: '#ef4444'
+ }
+ }}
+ >
+
+
+
+ )}
+
)}
-
- {loading && (
- <>
-
- {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((item, index) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
- >
+ ))}
+
+ {rows.length !== 0 && (
+
+
+
+ {isFetchingNextPage ? (
+
+ ) : hasNextPage ? (
+
+ ) : (
+
+ No more orders
+
+ )}
+
+
+
)}
-
- {rows?.length == 0 && (
- <>
-
-
-
-
-
- >
- )}
+
+
+
+
- {rows?.map((row, index) => {
- return (
- <>
-
-
- {page * rowsPerPage + index + 1}
-
-
-
-
- {row.locationname} {row.locationsuburb && `- ${row.locationsuburb}`}
-
-
- {row.orderid}
-
-
-
- {dayjs(row.pickupslot).format('DD/MM/YYYY')}
-
-
-
- {dayjs(row.pickupslot).format('hh:mm A')}
-
-
- {' '}
- {/*
-
-
- {dayjs(row.orderdate).utc().format('DD/MM/YYYY')}
-
-
- {dayjs(row.orderdate).utc().format('hh:mm A')}
-
-
- -
-
-
- {dayjs(row.deliverydate).utc().format('DD/MM/YYYY')}
-
-
- {dayjs(row.deliverydate).utc().format('hh:mm A')}
-
-
- */}
-
-
-
-
-
- {row.pickupcustomer}
- {row.pickupcontactno}
-
- {row.pickupsuburb || row.pickupaddress.slice(0, 20)}
-
-
-
-
-
-
-
- {row.deliverycustomer}
- {row.deliverycontactno}
-
-
- {row.deliverysuburb ||
- (row.deliveryaddress?.length > 20 ? `${row.deliveryaddress.slice(0, 20)}...` : row.deliveryaddress)}
-
-
-
-
-
-
-
-
- {row.quantity}
-
-
-
-
- ₹{row.collectionamt.toFixed(2)}
-
-
-
-
- {row.kms}
-
-
-
-
-
- ₹{row.deliverycharge.toFixed(2)}
-
-
-
- {row.ordernotes}
-
-
-
- {row.orderstatus === 'pending' && }
- {row.orderstatus === 'modified' && }
- {row.orderstatus === 'cancelled' && }
- {row.orderstatus === 'delivered' && }
- {row.orderstatus === 'processing' && }
- {row.orderstatus === 'ready' && }
- {row.orderstatus === 'confirmed' && }
-
- {row.orderstatus === 'active' && }
- {row.orderstatus === 'closed' && }
- {row.orderstatus === 'created' && }
-
-
-
- {currentStatus == 'created' && (
-
- {row.orderstatus == 'created' && (
- <>
-
- {
- e.stopPropagation();
- setOrderheaderid(row.orderheaderid);
- setCancelOpen(true);
- }}
- >
-
-
-
- >
- )}
-
- )}
-
- >
- );
- })}
- {rows?.length != 0 && (
-
-
-
- {isFetchingNextPage ? : hasNextPage ? : 'No More Orders'}
-
-
-
- )}
-
-
-
-
-
-
- {/* ============================================== || cancel order || ============================================== */}
-
+
+ {/* ============================================= || Table || ============================================= */}
+
-
+
-
- #
- Location
- All
-
- Orders{' '}
+
+ #
+ Location
+ All
+
+ Orders
-
- Deliveries{' '}
+
+ Deliveries
- Kilometer
- {/* Charges */}
- Amount
- Action
+ Kms
+ Amount
+ Action
- {/* sx={{ bgcolor: headCell.bgcolor }} */}
-
- Pending
- Completed
- Cancelled
- Pending
- Completed
- Cancelled
+
+ Pending
+ Completed
+ Cancelled
+ Pending
+ Completed
+ Cancelled
-
- {rows?.length !== 0 ? (
- rows?.map((row, index) => (
- <>
- {/* ============================================ || tablerow 1 || ============================================ */}
+
+ {rows && rows.length !== 0 ? (
+ rows.map((row, index) => (
+
+ {/* ===================== || Main row || ===================== */}
-
- {index + 1}
+
+ {index + 1}
-
-
- {row.locationname}
- Id : {row.locationid}
+
+
+
+
+ {row.locationname}
+
+
+ Id : {row.locationid}
+
-
- {row.totalorders}
-
-
- {row.Orderspending}
-
-
- {row.orderscompleted}
-
-
- {row.orderscancelled}
-
-
- {' '}
- {row.deliveriespending}
-
-
- {' '}
- {row.deliveriescompleted}
-
-
- {' '}
- {row.deliveriescancelled}
-
-
-
-
-
- {/*
-
-
- */}
+
+ } />
- {/* */}
- {/*
-
-
-
*/}
- {/*
-
- */}
- {/* */}
-
-
-
-
+
+
+ } />
- {
- getriderlocationsummary(row.locationid);
- setOpenRow(openRow === row.locationid ? null : row.locationid);
- }}
- sx={{
- bgcolor: openRow === row.locationid ? 'primary.main' : null,
- color: openRow === row.locationid ? 'white' : null,
- '&:hover': {
- bgcolor: openRow === row.locationid ? 'primary.main' : '#e1bee7'
- }
- }}
- >
- {openRow === row.locationid ? : }
-
+ } />
+
+
+ } />
+
+
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+
+
+ } isMoney />
+
+
+
+
+
+
+ {
+ getriderlocationsummary(row.locationid);
+ setOpenRow(openRow === row.locationid ? null : row.locationid);
+ }}
+ sx={{
+ bgcolor: openRow === row.locationid ? BRAND : tint(BRAND),
+ border: `1px solid ${openRow === row.locationid ? BRAND : edge(BRAND)}`,
+ color: openRow === row.locationid ? '#fff' : BRAND,
+ borderRadius: 999,
+ p: 0.75,
+ transition: 'all 0.18s',
+ '&:hover': {
+ bgcolor: openRow === row.locationid ? BRAND : soft(BRAND),
+ borderColor: BRAND,
+ boxShadow: `0 0 0 3px ${ring(BRAND)}`
+ }
+ }}
+ >
+ {openRow === row.locationid ? : }
+
+
- {/* ============================================ || collapsive row || ============================================ */}
- {openRow === row.locationid && (
-
-
-
-
-
-
-
- #
- Rider
- Deliveries
- Pending
- Assigned
- Accepted
- Arrived
- Picked
- Skipped
- Delivered
- kms
- {/* POD/PLA */}
- Charges
-
-
-
- {ridersdata?.map((rider, index) => (
-
-
- {index + 1}
-
-
-
- {rider?.firstname}
- {rider?.status == 'Active' ? (
-
- ) : (
-
- )}
-
-
-
- {rider?.totalorders}
-
-
- {rider?.pending}
-
-
- {rider?.assigned}
-
-
- {rider?.accepted}
-
-
- {rider?.picked}
-
-
- {rider?.arrived}
-
-
- {rider?.skipped}
-
-
- {rider?.delivered}
-
-
- {/* */}
-
- {/* */}
- {/*
-
- */}
-
- {/* */}
- {/*
-
- */}
- {/* */}
- {/* */}
- {/* */}
- {/* */}
-
-
-
-
-
+ {/* ===================== || Collapsible riders row || ===================== */}
+ {openRow === row.locationid && (
+
+
+
+
+
+
+
+
+
+ Riders Summary · {row.locationname}
+
+
+
+
+
+
+
+ #
+ Rider
+ Deliveries
+ Pending
+ Assigned
+ Accepted
+ Arrived
+ Picked
+ Skipped
+ Delivered
+ Kms
+ Charges
- ))}
-
-
+
+
+ {ridersdata && ridersdata.length > 0 ? (
+ ridersdata.map((rider, ri) => (
+
+
+
+ {ri + 1}
+
+
+
+
+
+ {rider?.firstname}
+
+ {rider?.status == 'Active' ? (
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+ } isMoney />
+
+
+ ))
+ ) : (
+
+
+
+
+
+
+
+ No rider data for this location
+
+
+
+
+ )}
+
+
+
)}
- >
+
))
) : (
-
-
-
+
+
+
+
+
+
+ No summary data
+
+
+ Adjust the date range or location filter above.
+
)}
+
+ {/* ===================== || Totals row || ===================== */}
+ {rows && rows.length !== 0 && (
+
+
+
+
+
+
+ Total
+
+
+
+ {totalOrders}
+
+
+ {totalOrderPend}
+
+
+ {totalOrderComplete}
+
+
+ {totalOrderCancel}
+
+
+ {totalDeliPend}
+
+
+ {totalDeliComplete}
+
+
+ {totalDeliCancel}
+
+
+
+
+ {formatNumberToRupees(total)}
+
+
+
+
+ )}
- {/* ============================================ || total - row || ============================================ */}
-
-
- Total
-
-
- {totalOrders}{' '}
-
-
- {totalOrderPend}{' '}
-
-
- {totalOrderComplete}{' '}
-
-
- {totalOrderCancel}
-
-
- {totalDeliPend}{' '}
-
-
- {totalDeliComplete}{' '}
-
-
- {totalDeliCancel}
-
-
-
- {formatNumberToRupees(total)}
- {' '}
-
-
-
- {/* ============================================ || date filter dialog || ============================================ */}
-
-
- Select Filter Options
-
+
+
+ {/* ============================================= || Date Filter Dialog || ============================================= */}
+ setOpen(false)} PaperProps={{ sx: { borderRadius: 3 } }}>
+
+
+
+
+
+
+
+ Select Date Range
+
+
+ Filter the summary by a date range or preset
+
+
+
+
-
- setOpen(false)}>
- ok
+
+ setOpen(false)}
+ sx={{
+ borderRadius: 999,
+ px: 2.5,
+ borderColor: DT.borderSubtle,
+ color: DT.textSecondary,
+ fontWeight: 700,
+ '&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
+ }}
+ >
+ Cancel
+
+ setOpen(false)}
+ sx={{
+ borderRadius: 999,
+ px: 3,
+ bgcolor: BRAND,
+ fontWeight: 700,
+ boxShadow: `0 6px 18px ${ring(BRAND)}`,
+ '&:hover': { bgcolor: '#4D1C61' }
+ }}
+ >
+ Apply
diff --git a/src/pages/nearle/reports/ordersDetails.js b/src/pages/nearle/reports/ordersDetails.js
index 344832c..7614773 100644
--- a/src/pages/nearle/reports/ordersDetails.js
+++ b/src/pages/nearle/reports/ordersDetails.js
@@ -1,83 +1,282 @@
-import { React, useState, useEffect, useRef } from 'react';
-import TitleCard from 'pages/nearle/titleCard';
+import React, { useState, useEffect, useRef, Fragment } from 'react';
import axios from 'axios';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
-import { Empty } from 'antd';
-import ClearIcon from '@mui/icons-material/Clear';
import { useTheme } from '@mui/material/styles';
-import { SlLocationPin } from 'react-icons/sl';
-import MapWithRoute from './mapWithRoute';
import { enqueueSnackbar } from 'notistack';
-import MyLocationIcon from '@mui/icons-material/MyLocation';
-// material-ui
import {
+ Avatar,
+ Box,
+ Button,
+ CircularProgress,
+ Dialog,
+ DialogContent,
Divider,
+ Grid,
+ IconButton,
+ InputBase,
+ Paper,
+ Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
- Dialog,
- DialogTitle,
- Typography,
- DialogContent,
- Stack,
- Button,
- IconButton,
Tooltip,
- Chip,
- FormControl,
- OutlinedInput,
- InputAdornment,
- Badge,
+ Typography,
Autocomplete,
TextField,
- CircularProgress,
- Box
+ Skeleton
} from '@mui/material';
-import { SearchOutlined } from '@ant-design/icons';
+import {
+ MdLocalShipping,
+ MdHourglassEmpty,
+ MdCheckCircle,
+ MdCancel,
+ MdMyLocation,
+ MdPlace,
+ MdSearch,
+ MdClear,
+ MdCalendarMonth,
+ MdReceiptLong,
+ MdStraighten,
+ MdCurrencyRupee,
+ MdInventory2,
+ MdHistoryToggleOff,
+ MdAccessTime,
+ MdInsights,
+ MdLocalOffer,
+ MdAssignmentTurnedIn,
+ MdFilterList
+} from 'react-icons/md';
+
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import { DateRangePicker } from 'mui-daterange-picker';
-import {
- addDays,
- addMonths,
- addWeeks,
- // addYears,
- endOfMonth,
- endOfWeek,
- // endOfYear,
- startOfMonth,
- startOfWeek
- // startOfYear,
-} from 'date-fns';
-import { CalendarMonth, FilterList } from '@mui/icons-material';
+import { addDays, addMonths, addWeeks, endOfMonth, endOfWeek, startOfMonth, startOfWeek } from 'date-fns';
-// project imports
import { fetchDeliverySummary, fetchorderdetails } from '../api/api';
-import MainCard from 'components/MainCard';
import { CSVExport } from 'components/third-party/ReactTable';
import Loader from 'components/Loader';
import { useDebounce } from 'components/nearle_components/useDebounce';
+import MapWithRoute from './mapWithRoute';
+
+// ============================================================================
+// Design tokens — shared with the rest of the redesigned operator pages.
+// ============================================================================
+const DT = {
+ radiusPill: 999,
+ radiusCard: 16,
+ shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
+ shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
+ shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
+ textPrimary: '#0f172a',
+ textSecondary: '#64748b',
+ textMuted: '#94a3b8',
+ borderSubtle: '#e2e8f0',
+ divider: '#f1f5f9',
+ surface: '#ffffff',
+ surfaceAlt: '#f8fafc'
+};
+const dtA = (c, suffix) => `${c}${suffix}`;
+const tint = (c) => dtA(c, '08');
+const soft = (c) => dtA(c, '18');
+const ring = (c) => dtA(c, '26');
+const edge = (c) => dtA(c, '55');
+
+const BRAND = '#662582';
+const BRAND_LIGHT = '#9255AB';
+
+// Semantic per-status palette — drives both status badges and the timeline cells.
+const STATUS_META = {
+ created: { label: 'Created', color: '#0ea5e9', icon: MdLocalShipping },
+ pending: { label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty },
+ accepted: { label: 'Accepted', color: '#6366f1', icon: MdAssignmentTurnedIn },
+ arrived: { label: 'Arrived', color: '#06b6d4', icon: MdCheckCircle },
+ picked: { label: 'Picked', color: '#8b5cf6', icon: MdLocalShipping },
+ active: { label: 'Active', color: '#14b8a6', icon: MdLocalShipping },
+ delivered: { label: 'Delivered', color: '#10b981', icon: MdCheckCircle },
+ skipped: { label: 'Skipped', color: '#f97316', icon: MdCancel },
+ cancelled: { label: 'Cancelled', color: '#ef4444', icon: MdCancel }
+};
+
+const STATUS_OPTIONS = [
+ { id: 0, status: 'All', statusLow: 'All', color: BRAND, icon: MdInsights },
+ { id: 1, status: 'Pending', statusLow: 'pending', color: '#f59e0b', icon: MdHourglassEmpty },
+ { id: 2, status: 'Accepted', statusLow: 'accepted', color: '#6366f1', icon: MdAssignmentTurnedIn },
+ { id: 3, status: 'Arrived', statusLow: 'arrived', color: '#06b6d4', icon: MdCheckCircle },
+ { id: 4, status: 'Picked', statusLow: 'picked', color: '#8b5cf6', icon: MdLocalShipping },
+ { id: 5, status: 'Active', statusLow: 'active', color: '#14b8a6', icon: MdLocalShipping },
+ { id: 6, status: 'Delivered', statusLow: 'delivered', color: '#10b981', icon: MdCheckCircle },
+ { id: 7, status: 'Skipped', statusLow: 'skipped', color: '#f97316', icon: MdCancel },
+ { id: 8, status: 'Cancelled', statusLow: 'cancelled', color: '#ef4444', icon: MdCancel }
+];
+
+const SoftPaper = (props) => (
+
+);
+
+const AccentAvatar = ({ color, selected, size = 24, children }) => (
+
+ {children}
+
+);
+
+const pillFieldSx = (color) => ({
+ '& .MuiOutlinedInput-root': {
+ borderRadius: DT.radiusPill + 'px',
+ bgcolor: tint(color),
+ fontWeight: 600,
+ '& fieldset': { borderColor: edge(color), borderWidth: 1.5 },
+ '&:hover fieldset': { borderColor: color },
+ '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(color)}` },
+ '&.Mui-focused fieldset': { borderColor: color, borderWidth: 2 }
+ }
+});
+
+const StatusBadge = ({ status }) => {
+ const meta = STATUS_META[String(status || '').toLowerCase()] || {
+ label: status || '—',
+ color: DT.textMuted,
+ icon: MdHistoryToggleOff
+ };
+ const Icon = meta.icon;
+ return (
+
+ {meta.label}
+
+ );
+};
+
+const MetricPill = ({ value, color, icon, isMoney = false }) => {
+ const n = Number(value);
+ const display = isMoney ? formatNumberToRupees(n) : Number.isFinite(n) ? n : value || 0;
+ const isZero = !Number.isFinite(n) || n === 0;
+ if (isZero) {
+ return (
+
+ {display}
+
+ );
+ }
+ return (
+
+ {icon}
+ {display}
+
+ );
+};
+
+// Compact timeline cell — shows event date+time with a coloured leading dot.
+const TimelineCell = ({ value, color }) => {
+ if (!value) {
+ return (
+
+ —
+
+ );
+ }
+ return (
+
+
+
+
+ {dayjs(value).format('DD/MM/YYYY')}
+
+
+ {dayjs(value).format('hh:mm A')}
+
+
+
+ );
+};
function formatNumberToRupees(value) {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2
- }).format(value);
+ }).format(value || 0);
}
-// ==============================|| MUI TABLE - ENHANCED ||============================== //
+const opentoast = (message, variant, time) => {
+ enqueueSnackbar(message, {
+ variant: variant,
+ anchorOrigin: { vertical: 'top', horizontal: 'right' },
+ autoHideDuration: time ? time : 1500
+ });
+};
+
+// ==============================|| OrdersDetails ||============================== //
export default function OrdersDetails() {
+ const theme = useTheme();
const tenId = localStorage.getItem('tenantid');
const textFieldRef = useRef(null);
const loadMoreRef = useRef();
const containerRef = useRef();
+
const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD'));
const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD'));
const [open, setOpen] = useState(false);
@@ -85,43 +284,29 @@ export default function OrdersDetails() {
const [totalCharge, settotalCharge] = useState(0);
const [totalAmount, settotalAmount] = useState(0);
const [searchword, setSearchword] = useState('');
- const theme = useTheme();
+
const [riderCoordinates, setRiderCoordinates] = useState([]);
const [riderStart, setRiderStart] = useState();
const [riderEnd, setRiderEnd] = useState();
const [mapOpen, setMapOpen] = useState(false);
const [mapTenant, setMapTenant] = useState({});
+
const [currentStatus, setCurrentStatus] = useState('All');
const [statusCount, setStatusCount] = useState(0);
+ const [statusValue, setStatusValue] = useState(STATUS_OPTIONS[0]);
+
const [tenantLocations, setTenantlocations] = useState([]);
const [locationId, setLocationId] = useState(0);
- const [locoName, setLocoName] = useState('Select Location');
+ const [locoName, setLocoName] = useState('All Locations');
+ const [selectedLocation, setSelectedLocation] = useState(null);
const debouncedSearch = useDebounce(searchword, 500);
- const status = [
- { id: 0, status: 'All', statusLow: 'All' },
- { id: 1, status: 'Pending', statusLow: 'pending' },
- { id: 2, status: 'Accepted', statusLow: 'accepted' },
- { id: 3, status: 'Arrived', statusLow: 'arrived' },
- { id: 4, status: 'Picked', statusLow: 'picked' },
- { id: 5, status: 'Active', statusLow: 'active' },
- { id: 6, status: 'Delivered', statusLow: 'delivered' },
- { id: 7, status: 'Skipped', statusLow: 'skipped' },
- { id: 8, status: 'Cancelled', statusLow: 'cancelled' }
- ];
- // ============================================= || gettenantlocations (branches) || =============================================
+ // ============================================= || gettenantlocations || =============================================
const gettenantlocations = async (id) => {
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/tenants/gettenantlocations/?tenantid=${id}`);
- console.log('gettenantlocations', res.data.details);
- if (res.data.details.length == 1) {
- setIsLocation(true);
- setTenantlocations(res.data.details);
- setPickCust(res.data.details[0]);
- } else {
- setTenantlocations(res.data.details);
- }
+ setTenantlocations(res.data.details || []);
} catch (err) {
console.log('gettenantlocations', err);
}
@@ -129,17 +314,16 @@ export default function OrdersDetails() {
useEffect(() => {
gettenantlocations(tenId);
}, []);
+
+ // ============================================= || getdeliverylogs (for map) || =============================================
const getdeliverylogs = async (id) => {
- console.log('deliveryid', id);
try {
const res = await axios.get(`${process.env.REACT_APP_URL}/deliveries/getdeliverylogs/?deliveryid=${id}`);
- console.log('getdeliverylogs', res.data.details);
const datas = res.data.details;
if (datas.length != 0) {
setRiderStart(datas[0].logdate);
setRiderEnd(datas[datas.length - 1].logdate);
const coData = datas.map((data) => ({ lat: data.latitude, lng: data.longitude }));
- console.log('coData', coData);
setRiderCoordinates(coData);
} else {
opentoast('No Logs Found ', 'error', 2000);
@@ -149,37 +333,22 @@ export default function OrdersDetails() {
}
};
- const opentoast = (message, variant, time) => {
- enqueueSnackbar(message, {
- variant: variant,
- anchorOrigin: { vertical: 'top', horizontal: 'right' },
- autoHideDuration: time ? time : 1500
- });
- };
-
- // ==============================|| textFieldRef (cmd+k)||============================== //
+ // ============================================= || ctrl/cmd+k focuses search || =============================================
useEffect(() => {
const handleKeyPress = (event) => {
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
-
- textFieldRef.current.focus();
+ textFieldRef.current && textFieldRef.current.focus();
}
if (event.key === 'Escape' && document.activeElement === textFieldRef.current) {
- // Remove focus from the TextField
textFieldRef.current.blur();
}
};
-
document.addEventListener('keydown', handleKeyPress);
-
- return () => {
- document.removeEventListener('keydown', handleKeyPress);
- };
+ return () => document.removeEventListener('keydown', handleKeyPress);
}, []);
- // ==============================|| fetchorderdetails (orders)||============================== //
-
+ // ============================================= || fetchorderdetails (infinite) || =============================================
const {
data: rowdata,
isError: isErrorOrderDetails,
@@ -188,7 +357,6 @@ export default function OrdersDetails() {
isLoading: isLoadingOrderDetails,
hasNextPage,
isFetchingNextPage
- // status: rowdataStatus
} = useInfiniteQuery({
queryKey: [startdate, enddate, currentStatus, locationId, debouncedSearch],
queryFn: fetchorderdetails,
@@ -206,7 +374,7 @@ export default function OrdersDetails() {
}
},
{
- root: document.querySelector('.MuiTableContainer-root'), // 👈 or explicitly TableContainer
+ root: document.querySelector('.MuiTableContainer-root'),
rootMargin: '0px',
threshold: 1.0
}
@@ -217,62 +385,51 @@ export default function OrdersDetails() {
};
}, [hasNextPage, fetchNextPage]);
- // ==================================== || fetchDeliverySummary || ====================================
+ const handleScroll = (event) => {
+ const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
+ if (scrollTop + clientHeight >= scrollHeight - 50) {
+ if (hasNextPage && !isFetchingNextPage) {
+ fetchNextPage();
+ }
+ }
+ };
+ // ============================================= || fetchDeliverySummary || =============================================
const { data: deliverycount } = useQuery({
queryKey: ['deliverycount', startdate, enddate, currentStatus, locationId],
queryFn: fetchDeliverySummary
});
useEffect(() => {
- console.log('currentStatus', currentStatus);
-
- currentStatus == 'All'
- ? setStatusCount(deliverycount?.total)
- : currentStatus == 'pending'
- ? setStatusCount(deliverycount?.pending)
- : currentStatus == 'accepted'
- ? setStatusCount(deliverycount?.accepted)
- : currentStatus == 'arrived'
- ? setStatusCount(deliverycount?.arrived)
- : currentStatus == 'picked'
- ? setStatusCount(deliverycount?.picked)
- : currentStatus == 'active'
- ? setStatusCount(deliverycount?.active)
- : currentStatus == 'delivered'
- ? setStatusCount(deliverycount?.delivered)
- : currentStatus == 'cancelled'
- ? setStatusCount(deliverycount?.cancelled)
- : setStatusCount(0);
+ const map = {
+ All: deliverycount?.total,
+ pending: deliverycount?.pending,
+ accepted: deliverycount?.accepted,
+ arrived: deliverycount?.arrived,
+ picked: deliverycount?.picked,
+ active: deliverycount?.active,
+ delivered: deliverycount?.delivered,
+ cancelled: deliverycount?.cancelled
+ };
+ setStatusCount(map[currentStatus] ?? 0);
}, [currentStatus, deliverycount]);
- // ==============================|| calculate||============================== //
-
- const calculate = () => {
- let calculatedTotalCharge = 0;
- let calculatedTotalAmount = 0;
- rows &&
- rows.forEach((row) => {
- calculatedTotalCharge += row.deliverycharges;
- });
- settotalCharge(calculatedTotalCharge);
- rows &&
- rows.forEach((row) => {
- calculatedTotalAmount += row.deliveryamt;
- });
- console.log('calculatedTotalAmount', calculatedTotalAmount);
- settotalAmount(calculatedTotalAmount);
- };
+ // ============================================= || calculate totals || =============================================
useEffect(() => {
- calculate();
+ let totalC = 0;
+ let totalA = 0;
+ rows &&
+ rows.forEach((row) => {
+ totalC += row.deliverycharges;
+ totalA += row.deliveryamt;
+ });
+ settotalCharge(totalC);
+ settotalAmount(totalA);
}, [rows]);
- // ==============================|| fetchAppLocations ||============================== //
-
if (isErrorOrderDetails) return 'An error has occurred:(isErrorOrderDetails) ' + orderDetailsError.message;
- // to download ex format filtered data
-
+ // CSV export rows.
const csvData = rows.map((order) => ({
tenantname: order.tenantname,
tenantcity: order.tenantcity,
@@ -313,451 +470,625 @@ export default function OrdersDetails() {
locationcontactno: order.locationcontactno
}));
- function formatDate(dateString) {
- const date = dayjs(dateString);
- const formattedDate = date.format('DD/MM/YYYY ');
- return formattedDate;
- }
- function formatTime(dateString) {
- const date = dayjs(dateString);
- const formattedDate = date.format(' hh:mm A');
- return formattedDate;
- }
- const handleScroll = (event) => {
- const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
- if (scrollTop + clientHeight >= scrollHeight - 50) {
- if (hasNextPage && !isFetchingNextPage) {
- fetchNextPage();
- }
- }
- };
+ // KPI tiles row.
+ const kpiCards = [
+ { key: 'total', label: `${currentStatus === 'All' ? 'Total' : currentStatus} Orders`, color: BRAND, icon: MdInsights, value: statusCount },
+ { key: 'pending', label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty, value: deliverycount?.pending ?? 0 },
+ { key: 'delivered', label: 'Delivered', color: '#10b981', icon: MdCheckCircle, value: deliverycount?.delivered ?? 0 },
+ { key: 'charges', label: 'Total Charges', color: '#6366f1', icon: MdLocalOffer, value: totalCharge, isMoney: true },
+ { key: 'amount', label: 'Total Amount', color: '#10b981', icon: MdCurrencyRupee, value: totalAmount, isMoney: true }
+ ];
+
return (
<>
{isLoadingOrderDetails && }
-
- {startdate && enddate && (
-
- {`Orders-${datestatus}`}
-
- {dayjs(startdate).format('DD/MM/YYYY')} - {dayjs(enddate).format('DD/MM/YYYY')}
-
- }
- variant="combined"
- color="warning"
- size="small"
- deleteIcon={}
- onDelete={() => {
- setOpen(true);
- }}
- onClick={() => setOpen(true)}
- sx={{ cursor: 'pointer' }}
- />
-
- )}
- {(!startdate || !enddate) && (
-
-
-
- {/* ALL} variant="combined" color='warning' size='small' /> */}
-
-
- )}
-
-
- }
- />
-
- {tenantLocations.length == 1 ? (
-
-
-
- )
+ {/* ============================================= || Header || ============================================= */}
+
+
+
+
+
+
+
+
+ Orders Details
+
+
+
+
+ Live · {locoName} · {datestatus}
+
+
+
+
+
+
+
+ setOpen(true)}
+ sx={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: 0.75,
+ px: 1.5,
+ py: 0.875,
+ borderRadius: 999,
+ cursor: 'pointer',
+ bgcolor: '#fff',
+ 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')}` }
+ }}
+ >
+
+ {startdate && enddate
+ ? `${dayjs(startdate).format('DD/MM/YY')} – ${dayjs(enddate).format('DD/MM/YY')}`
+ : 'All time'}
+
+
+ div, & > a, & > button': { color: BRAND, fontWeight: 700 }
+ }}
+ >
+
+
+
+
+
+
+ {/* ============================================= || KPI Cards || ============================================= */}
+
+ {kpiCards.map((item) => {
+ const Icon = item.icon;
+ return (
+
+
+
+
+
+
+ {item.label}
+
+
+ {isLoadingOrderDetails && !item.value ? (
+
+ ) : item.isMoney ? (
+ formatNumberToRupees(item.value)
+ ) : (
+ item.value ?? 0
+ )}
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ {/* ============================================= || Filter Bar || ============================================= */}
+
+
+ {/* Location filter */}
+
+ {tenantLocations.length === 1 ? (
+
+ {tenantLocations[0].locationname}
+
) : (
`${option.locationname} (${option.suburb})` || ''}
- renderInput={(params) => }
+ value={selectedLocation}
+ getOptionLabel={(option) => (option ? `${option.locationname} (${option.suburb || ''})` : '')}
+ PaperComponent={SoftPaper}
onChange={(event, value, reason) => {
if (value) {
- console.log('Business Locations', value);
+ setSelectedLocation(value);
setLocationId(value.locationid);
setLocoName(value.locationname);
}
- if (reason == 'clear') {
+ if (reason === 'clear') {
+ setSelectedLocation(null);
setLocationId(0);
- setLocoName('Select Location');
+ setLocoName('All Locations');
}
}}
+ renderInput={(params) => (
+
+
+
+
+
+ )
+ }}
+ />
+ )}
/>
)}
+
+ {/* Status filter */}
+
`${option.status}`}
+ options={STATUS_OPTIONS}
+ value={statusValue}
+ getOptionLabel={(option) => option?.status || ''}
+ PaperComponent={SoftPaper}
onChange={(event, value, reason) => {
if (reason === 'clear') {
setCurrentStatus('All');
+ setStatusValue(STATUS_OPTIONS[0]);
} else {
- console.log('status', value);
setCurrentStatus(value.statusLow);
+ setStatusValue(value);
}
}}
- renderInput={(params) => }
+ renderInput={(params) => (
+
+
+ {statusValue?.icon ? React.createElement(statusValue.icon, { size: 13 }) : }
+
+
+ )
+ }}
+ />
+ )}
/>
-
-
+
+ {/* Search */}
+
+
+
+
-
- {/*
- setOpen(true)}
- >
-
-
- */}
-
- }
+ />
+ {searchword && (
+ setSearchword('')} sx={{ p: 0.25, color: BRAND }}>
+
+
+ )}
+
+
+
+
+
+ {/* ============================================= || Table || ============================================= */}
+
-
-
-
- #
- {/* {} */}
- Location
- Pickup
- Drop
- Status
- Assigned
- Accepted
- Arrived
- Picked
- Delivered
- Cancelled
- {/* Notes */}
- Kms
- Charges
+
+
+
+ #
+ Location
+ Pickup
+ Drop
+ Status
+ Assigned
+ Accepted
+ Arrived
+ Picked
+ Delivered
+ Cancelled
+ Kms
+ Charges
+
- {rows?.length == 0 && (
- <>
-
-
-
+ {rows?.length === 0 && !isLoadingOrderDetails && (
+
+
+
+
+
+
+
+ No order details
+
+
+ {searchword ? 'Try a different keyword.' : 'Adjust the filters above to load orders.'}
+
- >
+
)}
- {rows?.map((row, index) => {
- return (
- rows?.length !== 0 && (
-
- {index + 1}
- {/* {
- console.log('row', row);
- setMapTenant(row);
- setMapOpen(true);
- getdeliverylogs(row.deliveryid);
- }}
- >
- {}
- */}
-
- {row.locationname}
-
- {row.orderid}
+
+ {isLoadingOrderDetails &&
+ rows.length === 0 &&
+ [0, 1, 2, 3, 4].map((_, idx) => (
+
+ {Array.from({ length: 13 }).map((__, ci) => (
+
+
+
+ ))}
+
+ ))}
+
+ {rows.map((row, index) => (
+
+
+ {index + 1}
+
+
+ {/* Location */}
+
+
+ {row.locationname}
+
+
+
+ {row.orderid}
+
+
+
+
+
+
+ {dayjs(row.deliverydate).utc().format('DD/MM/YYYY')} ·{' '}
+ {dayjs(row.deliverydate).utc().format('hh:mm A')}
-
-
-
- {dayjs(row.deliverydate).utc().format('DD/MM/YYYY')}
-
+
+
+
-
- {dayjs(row.deliverydate).utc().format('hh:mm A')}
-
-
-
-
-
-
-
- {row.pickupcustomer}
-
- {row.pickupcontactno}
-
-
-
- {row.pickupsuburb || row.pickuplocation || row.Pickupaddress.slice(0, 20)}
-
-
-
-
-
-
-
-
- {row.deliverycustomer}
-
- {row.deliverycontactno}
-
-
-
- {/* {row.Pickupaddress.slice(0, 20)} */}
- {row.deliverysuburb || row.deliverylocation || row.deliveryaddress.slice(0, 20)}
-
-
-
-
-
-
-
- {row.orderstatus === 'created' && }
- {row.orderstatus === 'pending' && }
- {row.orderstatus === 'accepted' && (
-
- )}
- {row.orderstatus === 'arrived' && (
-
- )}
- {row.orderstatus === 'picked' && }
- {row.orderstatus === 'active' && }
- {row.orderstatus === 'delivered' && }
- {row.orderstatus === 'skipped' && }
+ {/* Pickup */}
+
+
+
+ {row.pickupcustomer}
+
+
+ {row.pickupcontactno}
+
+
+
+ {row.pickupsuburb ||
+ row.pickuplocation ||
+ (row.Pickupaddress ? `${row.Pickupaddress.slice(0, 20)}…` : '—')}
+
+
+
+
- {row.orderstatus === 'cancelled' && }
-
-
-
-
+ {/* Drop */}
+
+
+
+ {row.deliverycustomer}
+
+
+ {row.deliverycontactno}
+
+
+
+ {row.deliverysuburb ||
+ row.deliverylocation ||
+ (row.deliveryaddress ? `${row.deliveryaddress.slice(0, 20)}…` : '—')}
+
+
+
+
+
+ {/* Order Status */}
+
+
+
+
+ {/* Assigned */}
+
+
+ {row.ridername && (
+
{row.ridername}
-
- {/* {dayjs(row.pending).format('DD/MM/YYYY')} */}
- {row.assigntime === '' ? '' : formatDate(row.assigntime)}
-
-
- {row.assigntime === '' ? '' : formatTime(row.assigntime)}
-
-
-
- {' '}
-
- {row.starttime === '' ? '' : formatDate(row.starttime)}
-
-
- {row.starttime === '' ? '' : formatTime(row.starttime)}
-
-
-
- {' '}
-
- {row.arrivaltime === '' ? '' : formatDate(row.arrivaltime)}
-
-
- {row.arrivaltime === '' ? '' : formatTime(row.arrivaltime)}
-
-
-
- {' '}
-
- {row.pickuptime === '' ? '' : formatDate(row.pickuptime)}
-
-
- {row.pickuptime === '' ? '' : formatTime(row.pickuptime)}
-
-
-
- {' '}
-
- {row.deliverytime === '' ? '' : formatDate(row.deliverytime)}
-
-
- {row.deliverytime === '' ? '' : formatTime(row.deliverytime)}
-
-
-
- {' '}
-
- {row.canceltime === '' ? '' : formatDate(row.canceltime)}
-
-
- {row.canceltime === '' ? '' : formatTime(row.canceltime)}
-
-
+ )}
+
+
+
- {/*
- {row.ordernotes}
- */}
+ {/* Accepted */}
+
+
+
-
-
-
+ {/* Arrived */}
+
+
+
- {/*
-
- */}
-
-
-
-
-
+ {/* Picked */}
+
+
+
- {/*
-
- */}
-
-
-
- )
- );
- })}
+ {/* Delivered */}
+
+
+
- {rows?.length != 0 && (
+ {/* Cancelled */}
+
+
+
+
+ {/* Kms */}
+
+ }
+ />
+
+
+ {/* Charges */}
+
+ }
+ isMoney
+ />
+
+
+ ))}
+
+ {rows.length !== 0 && (
-
+
- {isFetchingNextPage ? : hasNextPage ? : 'No More Orders'}
+ {isFetchingNextPage ? (
+
+ ) : hasNextPage ? (
+
+ ) : (
+
+ No more orders
+
+ )}
@@ -767,50 +1098,79 @@ export default function OrdersDetails() {
-
- {/* ========================================= || bottom row || ========================================= */}
-
-
- Total Charges :
-
-
-
-
-
+ Total Charges · {formatNumberToRupees(totalCharge)}
+
+
+ Total Amount · {formatNumberToRupees(totalAmount)}
+
+
+
+
+ {/* ============================================= || Date Filter Dialog || ============================================= */}
+ setOpen(false)} PaperProps={{ sx: { borderRadius: 3 } }}>
+
- Total Amount :
-
-
-
-
-
- {/* ========================================= || Date dialog || ========================================= */}
-
-
-
- Select Filter Options
-
+
+
+
+
+
+
+ Select Date Range
+
+
+ Filter the order details by a date range or preset
+
+
+
+
-
- setOpen(false)}>
- ok
+
+ setOpen(false)}
+ sx={{
+ borderRadius: 999,
+ px: 2.5,
+ borderColor: DT.borderSubtle,
+ color: DT.textSecondary,
+ fontWeight: 700,
+ '&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
+ }}
+ >
+ Cancel
+
+ setOpen(false)}
+ sx={{
+ borderRadius: 999,
+ px: 3,
+ bgcolor: BRAND,
+ fontWeight: 700,
+ boxShadow: `0 6px 18px ${ring(BRAND)}`,
+ '&:hover': { bgcolor: '#4D1C61' }
+ }}
+ >
+ Apply
- {/* ========================================= || MapWithRoute || ========================================= */}
+
+ {/* ============================================= || MapWithRoute Dialog || ============================================= */}
{
- setMapOpen(false);
- }}
- maxWidth={'lg'}
+ onClose={() => setMapOpen(false)}
+ maxWidth="lg"
fullWidth
+ PaperProps={{ sx: { borderRadius: 3, overflow: 'hidden' } }}
>
{riderCoordinates && (
-
+
-
+
)}
>
diff --git a/src/pages/nearle/reports/riderLogs.js b/src/pages/nearle/reports/riderLogs.js
index e11652a..74ac2d5 100644
--- a/src/pages/nearle/reports/riderLogs.js
+++ b/src/pages/nearle/reports/riderLogs.js
@@ -1,41 +1,100 @@
-import React, { useState, useEffect, Fragment } from 'react';
+import React, { useState, useEffect, useRef, Fragment } from 'react';
import {
- Box,
- Drawer,
- IconButton,
- Toolbar,
- Typography,
+ Avatar,
AppBar,
- useMediaQuery,
- Divider,
- List,
- ListItem,
- ListItemText,
- useTheme,
- ListItemAvatar,
- Stack,
+ Backdrop,
+ Box,
Button,
Checkbox,
+ Divider,
+ Drawer,
+ IconButton,
+ InputBase,
+ List,
+ ListItem,
+ Paper,
Skeleton,
- Backdrop,
- Chip
+ Stack,
+ Toolbar,
+ Typography,
+ useMediaQuery,
+ useTheme
} from '@mui/material';
-
-import MenuIcon from '@mui/icons-material/Menu';
-import SearchBar from 'components/nearle_components/SearchBar';
import { useQuery } from '@tanstack/react-query';
-import Loader from 'components/Loader';
+import dayjs from 'dayjs';
+
+import {
+ MdMenu,
+ MdSearch,
+ MdClear,
+ MdRefresh,
+ MdLocalShipping,
+ MdCheckCircle,
+ MdHighlightOff,
+ MdGroups,
+ MdAccessTime,
+ MdLocationOn,
+ MdMyLocation
+} from 'react-icons/md';
import RiderLocationMap from './RiderLocationMap';
-import MainCard from 'components/MainCard';
-import dayjs from 'dayjs';
-import TaskAltIcon from '@mui/icons-material/TaskAlt';
-
-import error500 from 'assets/images/maintenance/Error500.png';
import { fetchRidersLogs } from '../api/api';
import CircularLoader from 'components/nearle_components/CircularLoader';
+import error500 from 'assets/images/maintenance/Error500.png';
-const drawerWidth = 350;
+// ============================================================================
+// Design tokens — shared with the rest of the redesigned operator pages.
+// ============================================================================
+const DT = {
+ radiusPill: 999,
+ radiusCard: 16,
+ shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
+ shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
+ shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
+ textPrimary: '#0f172a',
+ textSecondary: '#64748b',
+ textMuted: '#94a3b8',
+ borderSubtle: '#e2e8f0',
+ divider: '#f1f5f9',
+ surface: '#ffffff',
+ surfaceAlt: '#f8fafc'
+};
+const dtA = (c, suffix) => `${c}${suffix}`;
+const tint = (c) => dtA(c, '08');
+const soft = (c) => dtA(c, '18');
+const ring = (c) => dtA(c, '26');
+const edge = (c) => dtA(c, '55');
+
+const BRAND = '#662582';
+const BRAND_LIGHT = '#9255AB';
+const C_ACTIVE = '#10b981';
+const C_INACTIVE = '#ef4444';
+
+const drawerWidth = 360;
+
+// Soft pill used for status / count chips throughout the page.
+const SoftPill = ({ color, icon, children, sx = {} }) => (
+
+ {icon}
+ {children}
+
+);
const RidersLogs = () => {
const theme = useTheme();
@@ -43,7 +102,9 @@ const RidersLogs = () => {
const [open, setOpen] = useState(false);
const [selectedRiders, setSelectedRiders] = useState([]);
const [riderSearch, setRiderSearch] = useState('');
+ const searchRef = useRef(null);
const appId = 1;
+
const {
data: riders,
isLoading: ridersIsLoading,
@@ -57,35 +118,58 @@ const RidersLogs = () => {
});
useEffect(() => {
- console.log('riders', riders);
- // const sortedRiders = riders?.sort((a, b) => a.firstname.localeCompare(b.firstname));
setSelectedRiders(riders);
}, [riders]);
- useEffect(() => {
- console.log('selectedRiders', selectedRiders);
- }, [selectedRiders]);
-
useEffect(() => {
setOpen(isDesktop);
}, [isDesktop]);
+ // Ctrl/Cmd+K focuses the rider search.
+ useEffect(() => {
+ const handleKeyPress = (event) => {
+ if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
+ event.preventDefault();
+ searchRef.current && searchRef.current.focus();
+ }
+ if (event.key === 'Escape' && document.activeElement === searchRef.current) {
+ searchRef.current.blur();
+ }
+ };
+ document.addEventListener('keydown', handleKeyPress);
+ return () => document.removeEventListener('keydown', handleKeyPress);
+ }, []);
+
+ // Counts shown in the drawer header.
+ const totalRiders = riders?.length || 0;
+ const activeCount = (riders || []).filter((r) => r.status === 'active').length;
+ const inactiveCount = totalRiders - activeCount;
+ const isAllSelected = totalRiders > 0 && selectedRiders?.length === totalRiders;
+
return (
- {
- theme.zIndex.drawer + 1
- }}
- open={ridersIsLoading || riderIsFetching} // when loader = true, backdrop covers the page
- >
-
-
- }
-
+ t.zIndex.drawer + 1
+ }}
+ open={ridersIsLoading || riderIsFetching}
+ >
+
+
+
+
- {/* Drawer */}
+ {/* ============================================= || Drawer || ============================================= */}
{
height: '100%',
overflowY: 'auto',
transition: 'transform 0.35s ease-in-out',
- zIndex: 13
+ zIndex: 13,
+ borderRight: `1px solid ${DT.borderSubtle}`,
+ backgroundColor: '#fff'
}
}}
>
- {/* 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);
- }
+ {/* ===== Drawer header — gradient strip with title + count pills ===== */}
+
+
+
+
+
+
+
+ Riders
+
+
+
-
-
-
-
-
+
+ Updated · {dayjs().format('hh:mm A')}
+
+
+
+
+
+ {/* Count pills */}
+
+ }>
+ Total · {totalRiders}
+
+ }>
+ Active · {activeCount}
+
+ }>
+ Inactive · {inactiveCount}
+
+
+
+ {/* Pill search */}
+
+
+ setRiderSearch(e.target.value)}
+ autoComplete="off"
+ sx={{
+ flex: 1,
+ fontSize: 13,
+ fontWeight: 600,
+ color: DT.textPrimary,
+ '& input::placeholder': { color: DT.textMuted, opacity: 1 }
+ }}
+ />
+ {riderSearch && (
+ setRiderSearch('')} sx={{ p: 0.25, color: BRAND }}>
+
+
+ )}
+
- {/* Rider List */}
-
- {/* Individuals */}
+
+ {/* ===== "All" selection pill ===== */}
+
+ {
+ if (!isAllSelected) {
+ setSelectedRiders(riders);
+ }
+ }}
+ sx={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: 1,
+ px: 1,
+ py: 0.75,
+ borderRadius: 999,
+ cursor: 'pointer',
+ border: `1.5px solid ${isAllSelected ? BRAND : edge(BRAND)}`,
+ bgcolor: isAllSelected ? BRAND : tint(BRAND),
+ color: isAllSelected ? '#fff' : BRAND,
+ transition: 'all 0.18s',
+ boxShadow: isAllSelected ? `0 6px 18px ${ring(BRAND)}` : 'none',
+ '&:hover': {
+ borderColor: BRAND,
+ boxShadow: `0 0 0 3px ${ring(BRAND)}`
+ }
+ }}
+ >
+ {
+ if (e.target.checked) {
+ setSelectedRiders(riders);
+ }
+ }}
+ sx={{
+ p: 0,
+ color: isAllSelected ? '#fff' : BRAND,
+ '&.Mui-checked': { color: '#fff' }
+ }}
+ />
+
+ Show All Riders
+
+
+ {totalRiders}
+
+
+
+
+ {/* ===== Rider list ===== */}
+
{ridersIsLoading || riderIsFetching
- ? Array.from({ length: 10 }).map((_, index) => (
-
-
-
-
-
-
- }
- secondary={}
- />
-
-
-
-
+ ? Array.from({ length: 8 }).map((_, index) => (
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
))
: riders?.map((row) => {
+ const isSelected = selectedRiders?.length === 1 && selectedRiders[0]?.userid === row?.userid;
+ const statusColor = row.status === 'active' ? C_ACTIVE : C_INACTIVE;
+ const initial = (row.firstname || row.username || '?').charAt(0).toUpperCase();
+ const name =
+ row.username && row.username.length > 0
+ ? `${row.username.slice(0, 25)}${row.username.length > 25 ? '…' : ''}`
+ : `${row.firstname || ''}${row.lastname ? ` ${row.lastname}` : ''}`.trim() || 'Rider';
+
return (
-
-
-
- {row.userid}
-
-
- {dayjs(row.logdate).format('DD/MM/YYYY hh:mm A')}
-
-
+ {
+ // Tapping the card selects this rider in isolation.
+ setSelectedRiders([row]);
+ }}
+ sx={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: 1,
+ p: 1,
+ mb: 0.625,
+ borderRadius: 2,
+ cursor: 'pointer',
+ border: `1px solid ${isSelected ? edge(BRAND) : DT.divider}`,
+ bgcolor: isSelected ? tint(BRAND) : '#fff',
+ transition: 'all 0.15s',
+ '&:hover': {
+ borderColor: edge(BRAND),
+ bgcolor: isSelected ? soft(BRAND) : DT.surfaceAlt,
+ boxShadow: DT.shadowSoft
}
+ }}
+ >
+ e.stopPropagation()}
+ onChange={(e) => {
+ if (e.target.checked) {
+ setSelectedRiders([row]);
+ } else {
+ setSelectedRiders(riders);
+ }
+ }}
+ sx={{
+ p: 0.5,
+ color: edge(BRAND),
+ '&.Mui-checked': { color: BRAND }
+ }}
+ />
+
+
-
- {
- 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' && } */}
-
- ) : (
-
- {row.firstname || ''}
- {row.lastname ? ` ${row.lastname}` : ''}
-
- )
- }
- secondary={
-
- {row.contactno || '##########'}
-
- }
+ {initial}
+
-
+
-
-
+
+
+ {name}
+
+
+
+ {row.contactno || '##########'}
+
+
+
+
+
+ : }
+ sx={{ fontSize: 10 }}
+ >
+ {row.status === 'active' ? 'Active' : 'Inactive'}
+
+
+
+
+ {row.logdate ? dayjs(row.logdate).format('DD/MM · hh:mm A') : '—'}
+
+
+
+
);
})}
+
+ {!ridersIsLoading && !riderIsFetching && totalRiders === 0 && (
+
+
+
+
+
+ No riders to show
+
+
+ {riderSearch ? 'Try a different rider name.' : 'Pull-to-refresh once you have rider activity.'}
+
+
+ )}
- {/* AppBar */}
+ {/* ============================================= || AppBar || ============================================= */}
{
left: open && isDesktop ? `${drawerWidth}px` : 0,
width: open && isDesktop ? `calc(100% - ${drawerWidth}px)` : '100%',
transition: 'left 0.3s ease, width 0.3s ease',
- backgroundColor: 'white',
- borderBottom: '1px solid',
- borderColor: theme.palette.secondary.light
+ background: `linear-gradient(135deg, ${tint(BRAND)} 0%, ${tint(BRAND_LIGHT)} 100%)`,
+ borderBottom: `1px solid ${DT.borderSubtle}`,
+ color: DT.textPrimary
}}
>
-
+
-
- setOpen(!open)}>
-
+
+ setOpen(!open)}
+ sx={{
+ bgcolor: '#fff',
+ border: `1px solid ${DT.borderSubtle}`,
+ borderRadius: 999,
+ color: BRAND,
+ '&:hover': { bgcolor: tint(BRAND), borderColor: edge(BRAND) }
+ }}
+ >
+
-
-
- Riders Locations
-
+
+
+
+
+
+ Riders Locations
+
+
+
+
+ Live · {selectedRiders?.length || 0} of {totalRiders} on map
+
+
+
- {
- riderLogsRefetch();
- }}
- >
- Refresh
-
+
+ } sx={{ display: { xs: 'none', sm: 'inline-flex' } }}>
+ {dayjs().format('DD MMM YYYY')}
+
+ riderLogsRefetch()}
+ startIcon={}
+ sx={{
+ borderRadius: 999,
+ px: 2,
+ bgcolor: BRAND,
+ fontWeight: 700,
+ boxShadow: `0 6px 18px ${ring(BRAND)}`,
+ textTransform: 'none',
+ '&:hover': { bgcolor: '#4D1C61' }
+ }}
+ >
+ Refresh
+
+
- {/* Map */}
+ {/* ============================================= || Map area || ============================================= */}
{(ridersIsLoading || riderIsFetching) && (
- {/* */}
{
position: 'absolute',
top: 0,
left: 0,
- borderRadius: 1,
+ borderRadius: 0,
zIndex: 1
}}
/>
)}
- {selectedRiders?.length > 0 && }
+ {selectedRiders?.length > 0 && !riderLogsError && }
+
{riderLogsError && (
-
-
-
+
+
+
+
+
+
+ Couldn’t load rider logs
+
+
+ The map is unavailable right now. Try refreshing in a moment.
+
+
+ riderLogsRefetch()}
+ startIcon={}
+ sx={{
+ borderRadius: 999,
+ px: 3,
+ bgcolor: BRAND,
+ fontWeight: 700,
+ boxShadow: `0 6px 18px ${ring(BRAND)}`,
+ textTransform: 'none',
+ '&:hover': { bgcolor: '#4D1C61' }
+ }}
+ >
+ Retry
+
+
+
+
+
)}
-
+
);
};
diff --git a/src/pages/nearle/reports/ridersummary.js b/src/pages/nearle/reports/ridersummary.js
index b091af2..cffad34 100644
--- a/src/pages/nearle/reports/ridersummary.js
+++ b/src/pages/nearle/reports/ridersummary.js
@@ -1,563 +1,972 @@
-import { React, useState, useEffect, useRef } from 'react';
+import React, { useState, useEffect, useRef, Fragment } from 'react';
import axios from 'axios';
import { useQuery } from '@tanstack/react-query';
-import TaskAltIcon from '@mui/icons-material/TaskAlt';
-import HighlightOffIcon from '@mui/icons-material/HighlightOff';
-import { Empty } from 'antd';
-// material-ui
+
import {
+ Avatar,
Box,
+ Button,
+ Collapse,
+ Dialog,
+ DialogContent,
Divider,
+ Grid,
+ IconButton,
+ InputBase,
+ Paper,
+ Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
- Dialog,
- DialogTitle,
- Typography,
- DialogContent,
- Stack,
- Button,
- IconButton,
Tooltip,
- Chip,
- Collapse
+ Typography,
+ Skeleton
} from '@mui/material';
-import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
-import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
+import {
+ MdLocalShipping,
+ MdHourglassEmpty,
+ MdCheckCircle,
+ MdCancel,
+ MdSearch,
+ MdClear,
+ MdCalendarMonth,
+ MdReceiptLong,
+ MdStraighten,
+ MdCurrencyRupee,
+ MdInventory2,
+ MdInsights,
+ MdLocalOffer,
+ MdKeyboardArrowDown,
+ MdKeyboardArrowUp,
+ MdAssignmentTurnedIn,
+ MdGroups,
+ MdAccessTime,
+ MdTaskAlt,
+ MdHighlightOff,
+ MdPlace
+} from 'react-icons/md';
+
import dayjs from 'dayjs';
var utc = require('dayjs/plugin/utc');
dayjs.extend(utc);
import { DateRangePicker } from 'mui-daterange-picker';
-import {
- addDays,
- addMonths,
- addWeeks,
- // addYears,
- endOfMonth,
- endOfWeek,
- // endOfYear,
- startOfMonth,
- startOfWeek
- // startOfYear,
-} from 'date-fns';
-// project imports
-import MainCard from 'components/MainCard';
+import { addDays, addMonths, addWeeks, endOfMonth, endOfWeek, startOfMonth, startOfWeek } from 'date-fns';
+
import Loader from 'components/Loader';
-import TitleCard from '../titleCard';
-import { fetchRidersSummary } from '../api/api';
import { useTheme } from '@mui/material/styles';
-import { getValueColor } from 'components/nearle_components/getValueColor';
+import { fetchRidersSummary } from '../api/api';
-// table filter
-function descendingComparator(a, b, orderBy) {
- if (b[orderBy] < a[orderBy]) {
- return -1;
+// ============================================================================
+// Design tokens — shared with the rest of the redesigned operator pages.
+// ============================================================================
+const DT = {
+ radiusPill: 999,
+ radiusCard: 16,
+ shadowSoft: '0 14px 40px rgba(15, 23, 42, 0.10)',
+ shadowMd: '0 8px 24px rgba(15, 23, 42, 0.08)',
+ shadowPop: '0 18px 50px rgba(15, 23, 42, 0.18)',
+ textPrimary: '#0f172a',
+ textSecondary: '#64748b',
+ textMuted: '#94a3b8',
+ borderSubtle: '#e2e8f0',
+ divider: '#f1f5f9',
+ surface: '#ffffff',
+ surfaceAlt: '#f8fafc'
+};
+const dtA = (c, suffix) => `${c}${suffix}`;
+const tint = (c) => dtA(c, '08');
+const soft = (c) => dtA(c, '18');
+const ring = (c) => dtA(c, '26');
+const edge = (c) => dtA(c, '55');
+
+const BRAND = '#662582';
+const BRAND_LIGHT = '#9255AB';
+
+// Semantic colour palette per metric column.
+const C_PENDING = '#f59e0b';
+const C_ASSIGNED = '#0ea5e9';
+const C_ACCEPTED = '#6366f1';
+const C_ARRIVED = '#06b6d4';
+const C_PICKED = '#8b5cf6';
+const C_ACTIVE = '#14b8a6';
+const C_SKIPPED = '#f97316';
+const C_CANCELLED = '#ef4444';
+const C_DELIVERED = '#10b981';
+const C_KMS = '#f59e0b';
+const C_COD = '#ef4444';
+const C_PLA = '#10b981';
+
+const AccentAvatar = ({ color, selected, size = 24, children }) => (
+
+ {children}
+
+);
+
+// Inline metric pill used in cells; faded slate when zero.
+const MetricPill = ({ value, color, icon, isMoney = false, minWidth }) => {
+ const n = Number(value);
+ const display = isMoney ? formatNumberToRupees(n) : Number.isFinite(n) ? n : value || 0;
+ const isZero = !Number.isFinite(n) || n === 0;
+ if (isZero) {
+ return (
+
+ {display}
+
+ );
}
- if (b[orderBy] > a[orderBy]) {
- return 1;
- }
- return 0;
-}
+ return (
+
+ {icon}
+ {display}
+
+ );
+};
-function getComparator(order, orderBy) {
- return order === 'desc' ? (a, b) => descendingComparator(a, b, orderBy) : (a, b) => -descendingComparator(a, b, orderBy);
-}
-
-function stableSort(array, comparator) {
- const stabilizedThis = array.map((el, index) => [el, index]);
- stabilizedThis.sort((a, b) => {
- const order = comparator(a[0], b[0]);
- if (order !== 0) return order;
- return a[1] - b[1];
- });
- return stabilizedThis.map((el) => el[0]);
-}
function formatNumberToRupees(value) {
return new Intl.NumberFormat('en-IN', {
style: 'currency',
currency: 'INR',
minimumFractionDigits: 2
- }).format(value);
+ }).format(value || 0);
}
-// ==============================|| MUI TABLE - ENHANCED ||============================== //
+// ==============================|| RidersSummary ||============================== //
export default function RidersSummary() {
- // const [rows, setRows] = useState([]);
const theme = useTheme();
const tenantid = localStorage.getItem('tenantid');
- const [order, setOrder] = useState('asc');
- const [orderBy, setOrderBy] = useState('calories');
- const [selected, setSelected] = useState([]);
- const [page, setPage] = useState(0);
- const [dense] = useState(false);
- const [rowsPerPage, setRowsPerPage] = useState(10);
- const [selectedValue, setSelectedValue] = useState([]);
+
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 [dateselect, setDateselect] = useState('select');
- const [tabstatus1, setTabstatus1] = useState('Today');
const [datestatus, setDatestatus] = useState('Today');
const [total, settotal] = useState(0);
- const [id, setid] = useState(-1);
-
const [tenantData, setTenantData] = useState([]);
- const [openRow, setOpenRow] = useState(null); // Initially no row is open
+ const [openRow, setOpenRow] = useState(null);
const [searchword, setSearchword] = useState('');
+ const [debouncedSearch, setDebouncedSearch] = useState('');
const textFieldRef = useRef(null);
- const [appId, setAppId] = useState(localStorage.getItem('applocationid'));
- const [locations, setLocations] = useState('Select Location');
- const userid = localStorage.getItem('userid');
+ // Debounce client-side search on rider name.
+ useEffect(() => {
+ const handler = setTimeout(() => setDebouncedSearch(searchword), 350);
+ return () => clearTimeout(handler);
+ }, [searchword]);
- /* ============================================= || handleKeyPress (ctrl+k)| ============================================= */
+ // Ctrl/Cmd+K focuses search.
useEffect(() => {
const handleKeyPress = (event) => {
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
-
- textFieldRef.current.focus();
+ textFieldRef.current && textFieldRef.current.focus();
}
if (event.key === 'Escape' && document.activeElement === textFieldRef.current) {
- // Remove focus from the TextField
textFieldRef.current.blur();
}
};
document.addEventListener('keydown', handleKeyPress);
-
- return () => {
- document.removeEventListener('keydown', handleKeyPress);
- };
+ return () => document.removeEventListener('keydown', handleKeyPress);
}, []);
- // ==============================|| fetchRidersSummary (riders summary)||============================== //
+ // ============================================= || fetchRidersSummary || =============================================
const {
isLoading: isLoadingReports,
- isError: isErrorReports, //true or false
+ isError: isErrorReports,
data: rows,
error: reportsError
} = useQuery({
queryKey: [tenantid, startdate, enddate],
queryFn: fetchRidersSummary
});
+
+ // ============================================= || calculate totals || =============================================
useEffect(() => {
- rows && console.log('rows', rows);
- }, [rows]);
- // ==============================|| calculate||============================== //
- const calculate = async () => {
+ if (!rows) return;
let calculatedTotal = 0;
- rows &&
- rows.forEach((row, index) => {
- console.log(index, row.Deliveryamt);
- calculatedTotal += row.Deliveryamt;
- });
- // Update the state after the calculation is done
+ rows.forEach((row) => {
+ calculatedTotal += Number(row.Deliveryamt || row.deliveryamt || 0);
+ });
settotal(calculatedTotal);
- console.log('calculatedTotal', calculatedTotal);
- };
- useEffect(() => {
- rows && calculate();
}, [rows]);
- if (isLoadingReports) return ;
- if (isErrorReports) return 'An error has occurred:(isErrorReports) ' + reportsError.message;
-
- // ==============================|| fetchTenantSummary by rider (rider summary)||============================== //
+ // ============================================= || fetchTenantSummary (per rider) || =============================================
const fetchTenantSummary = async (riderUserid) => {
try {
const tenantRes = await axios.get(
- // `${process.env.REACT_APP_URL}/deliveries/getreportlocationsummary/?&fromdate=${startdate}&todate=${enddate}&userid=${riderUserid}&tenantid=${tenantid}`
`${process.env.REACT_APP_URL}/deliveries/getriderlocationreportsummary/?&fromdate=${startdate}&todate=${enddate}&userid=${riderUserid}&tenantid=${tenantid}`
);
- console.log('tenantRes', tenantRes.data.details);
- setTenantData(tenantRes.data.details);
+ setTenantData(tenantRes.data.details || []);
} catch (error) {
console.log('tenantRes', error);
}
};
+ if (isErrorReports) return 'An error has occurred:(isErrorReports) ' + reportsError.message;
+
+ // Client-side filter on rider name.
+ const filteredRows = (rows || []).filter((r) => {
+ const q = debouncedSearch.trim().toLowerCase();
+ if (!q) return true;
+ return (
+ String(r.firstname || '').toLowerCase().includes(q) ||
+ String(r.lastname || '').toLowerCase().includes(q) ||
+ String(r.userid || '').toLowerCase().includes(q)
+ );
+ });
+
+ // KPI totals — aggregate across filtered rows.
+ const kpiTotals = filteredRows.reduce(
+ (acc, r) => {
+ acc.deliveries += Number(r.totalorders || 0);
+ acc.pending += Number(r.pending || 0);
+ acc.delivered += Number(r.delivered || 0);
+ acc.charges += Number(r.deliveryamt || 0);
+ return acc;
+ },
+ { deliveries: 0, pending: 0, delivered: 0, charges: 0 }
+ );
+
+ const kpiCards = [
+ { key: 'riders', label: 'Riders', color: BRAND, icon: MdGroups, value: filteredRows.length },
+ { key: 'deliveries', label: 'Deliveries', color: C_ASSIGNED, icon: MdLocalShipping, value: kpiTotals.deliveries },
+ { key: 'pending', label: 'Pending', color: C_PENDING, icon: MdHourglassEmpty, value: kpiTotals.pending },
+ { key: 'delivered', label: 'Delivered', color: C_DELIVERED, icon: MdCheckCircle, value: kpiTotals.delivered },
+ { key: 'charges', label: 'Total Charges', color: BRAND, icon: MdLocalOffer, value: kpiTotals.charges, isMoney: true }
+ ];
+
return (
<>
-
-
-
- {startdate && enddate && (
-
-
-
- {dayjs(startdate).format('DD/MM/YYYY')} - {dayjs(enddate).format('DD/MM/YYYY')}
-
- }
- variant="combined"
- color="warning"
- size="small"
- />
-
- )}
- {(!startdate || !enddate) && (
- <>
-
-
- {/* ALL} variant="combined" color='warning' size='small' /> */}
-
- >
- )}
+ {isLoadingReports && }
+
+ {/* ============================================= || Header || ============================================= */}
+
+
+
+
+
+
+
+
+ Riders Summary
+
+
+
+
+ Live · {filteredRows.length} riders · {datestatus}
+
+
- }
+
+
+ setOpen(true)}
+ sx={{
+ display: 'inline-flex',
+ alignItems: 'center',
+ gap: 0.75,
+ px: 1.5,
+ py: 0.875,
+ borderRadius: 999,
+ cursor: 'pointer',
+ bgcolor: '#fff',
+ 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')}` }
+ }}
+ >
+
+ {startdate && enddate
+ ? `${dayjs(startdate).format('DD/MM/YY')} – ${dayjs(enddate).format('DD/MM/YY')}`
+ : 'All time'}
+
+
+
+
+
+ {/* ============================================= || KPI Cards || ============================================= */}
+
+ {kpiCards.map((item) => {
+ const Icon = item.icon;
+ return (
+
+
+
+
+
+
+ {item.label}
+
+
+ {isLoadingReports ? (
+
+ ) : item.isMoney ? (
+ formatNumberToRupees(item.value)
+ ) : (
+ item.value
+ )}
+
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ {/* ============================================= || Filter Bar || ============================================= */}
+
- {/* table */}
-
-
+
+
+
+
+ setSearchword(e.target.value)}
+ autoComplete="off"
+ sx={{
+ flex: 1,
+ fontSize: 13,
+ fontWeight: 600,
+ color: DT.textPrimary,
+ '& input::placeholder': { color: DT.textMuted, opacity: 1 }
+ }}
+ />
+ {searchword && (
+ setSearchword('')} sx={{ p: 0.25, color: BRAND }}>
+
+
+ )}
+
+
+
+
+
+ Orders · {datestatus}
+
+ {startdate && enddate && (
+
+ {dayjs(startdate).format('DD/MM/YY')} – {dayjs(enddate).format('DD/MM/YY')}
+
+ )}
+
+
+
+
+ {/* ============================================= || Table || ============================================= */}
+
+
+
-
- #
- Rider
- Deliveries
- Pending
- Assigned
- Accepted
- Arrived
- Picked
- Active
- Skipped
- Cancelled
- Delivered
- kms
- COD/PLA
- Charges
- Action
+
+ #
+ Rider
+ Deliveries
+ Pending
+ Assigned
+ Accepted
+ Arrived
+ Picked
+ Active
+ Skipped
+ Cancelled
+ Delivered
+ Kms
+ COD / PLA
+ Charges
+ Action
- {/* ============================================ || TableBody || ============================================ */}
- {/* {rows?.length === 0 && (
+ {isLoadingReports &&
+ filteredRows.length === 0 &&
+ [0, 1, 2, 3, 4].map((_, idx) => (
+
+ {Array.from({ length: 16 }).map((__, ci) => (
+
+
+
+ ))}
+
+ ))}
+
+ {!isLoadingReports && filteredRows.length === 0 && (
-
-
-
+
+
+
+
+
+
+ No riders to show
+
+
+ {searchword ? 'Try a different rider name.' : 'Adjust the date range above.'}
+
- )} */}
-
- {rows?.map((row, index) => (
- <>
- {/* // ============================================ || tablerow 1 || ============================================ */}
+ )}
+ {filteredRows.map((row, index) => (
+
+ {/* ===================== || Main rider row || ===================== */}
-
- {index + 1}
+
+ {index + 1}
-
-
-
- {row.firstname} {row.lastname}
-
- Id : {row.userid}
+
+
+
+
+ {String(row.firstname || '?').charAt(0).toUpperCase()}
+
+
+
+ {row.firstname} {row.lastname}
+
+
+ Id : {row.userid}
+
+
-
- {row.totalorders}
+
+ } />
- {row.pending ? : row.pending}
+ } />
- {row.assigned ? : row.assigned}
+ } />
- {row.accepted ? : row.accepted}
+ } />
- {row.arrived ? : row.arrived}
+ } />
- {row.picked ? : row.picked}
+ } />
- {row.active ? : row.active}
+ } />
- {row.skipped ? : row.skipped}
+ } />
- {row.cancelled ? : row.cancelled}
+ } />
+
+
+ } />
-
- {row.delivered}
-
-
-
-
- {/*
-
- */}
-
-
+
+
+ }
/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
- {
- fetchTenantSummary(row.userid);
- setOpenRow(openRow === row.userid ? null : row.userid);
- }}
- sx={{
- bgcolor: openRow === row.userid ? 'primary.main' : null,
- color: openRow === row.userid ? 'white' : null,
- '&:hover': {
- bgcolor: openRow === row.userid ? 'primary.main' : '#e1bee7'
- }
- }}
- >
- {openRow === row.userid ? : }
-
+
+
+
+ } isMoney />
+
+
+
+
+ } isMoney />
+
+
+
+
+
+
+
+
+ } isMoney />
+
+
+
+
+
+
+ {
+ fetchTenantSummary(row.userid);
+ setOpenRow(openRow === row.userid ? null : row.userid);
+ }}
+ sx={{
+ bgcolor: openRow === row.userid ? BRAND : tint(BRAND),
+ border: `1px solid ${openRow === row.userid ? BRAND : edge(BRAND)}`,
+ color: openRow === row.userid ? '#fff' : BRAND,
+ borderRadius: 999,
+ p: 0.75,
+ transition: 'all 0.18s',
+ '&:hover': {
+ bgcolor: openRow === row.userid ? BRAND : soft(BRAND),
+ borderColor: BRAND,
+ boxShadow: `0 0 0 3px ${ring(BRAND)}`
+ }
+ }}
+ >
+ {openRow === row.userid ? : }
+
+
- {/* // ============================================ || collapsive row || ============================================ */}
- {openRow === row.userid && (
-
-
-
-
-
-
-
- #
- Location
- All
- Pending
- Completed
- Cancelled
- Kms
- COD / PLA
- Amount
-
-
-
- {tenantData?.map((row, index) => (
+ {/* ===================== || Collapsible per-location summary || ===================== */}
+ {openRow === row.userid && (
+
+
+
+
+
+
+
+
+
+ Locations · {row.firstname} {row.lastname}
+
+
+
+
+
+
-
- {index + 1}
-
-
-
- {row.locationname}
- Id: {row.locationid}
-
-
-
- {row.totalorders}
-
-
- {row.deliveriespending}
-
-
- {row.deliveriescompleted}
-
-
- {row.deliveriescancelled}
-
-
- {/* */}
-
-
-
-
-
-
- {' '}
-
-
-
-
-
-
+ #
+ Location
+ All
+ Pending
+ Completed
+ Cancelled
+ Kms
+ COD / PLA
+ Amount
- ))}
-
-
+
+
+ {tenantData && tenantData.length > 0 ? (
+ tenantData.map((subrow, sridx) => (
+
+
+
+ {sridx + 1}
+
+
+
+
+
+ {subrow.locationname}
+
+
+ Id : {subrow.locationid}
+
+
+
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+ } />
+
+
+ }
+ />
+
+
+
+
+
+ } isMoney />
+
+
+
+
+ } isMoney />
+
+
+
+
+
+ } isMoney />
+
+
+ ))
+ ) : (
+
+
+
+
+
+
+
+ No location data for this rider
+
+
+
+
+ )}
+
+
+
)}
- >
+
))}
+
- {/*
- Total :
-
- {formatNumberToRupees(total)}
-
- */}
-
- {/* ================================================ || Date Filter || ================================================ */}
-
-
- Select Filter Options
-
+
+ {/* ===================== || Footer total strip || ===================== */}
+ {filteredRows.length > 0 && (
+
+
+ Total Amount · {formatNumberToRupees(total)}
+
+
+ )}
+
+
+ {/* ============================================= || Date Filter Dialog || ============================================= */}
+ setOpen(false)} PaperProps={{ sx: { borderRadius: 3 } }}>
+
+
+
+
+
+
+
+ Select Date Range
+
+
+ Filter rider activity by a date range or preset
+
+
+
+
{
if (range.label === 'All') {
- setDateselect('all');
setStartdate('');
setEnddate('');
+ setDatestatus('All');
setOpen(false);
} else {
- setDateselect('select');
setStartdate(dayjs(range.startDate).format('YYYY-MM-DD'));
setEnddate(dayjs(range.endDate).format('YYYY-MM-DD'));
- if (range.label) {
- setDatestatus(range.label);
- } else {
- setDatestatus('');
- }
+ setDatestatus(range.label || '');
}
- console.log(range);
}}
definedRanges={[
- {
- label: 'Today',
- startDate: new Date(),
- endDate: new Date()
- },
- {
- label: 'Yesterday',
- startDate: addDays(new Date(), -1),
- endDate: addDays(new Date(), -1)
- },
- {
- label: 'Tomorrow',
- startDate: addDays(new Date(), +1),
- endDate: addDays(new Date(), +1)
- },
- {
- label: 'This Week',
- startDate: startOfWeek(new Date()),
- endDate: endOfWeek(new Date())
- },
- {
- label: 'Last Week',
- startDate: startOfWeek(addWeeks(new Date(), -1)),
- endDate: endOfWeek(addWeeks(new Date(), -1))
- },
- {
- label: 'Last 7 Days',
- startDate: addWeeks(new Date(), -1),
- endDate: new Date()
- },
- {
- label: 'This Month',
- startDate: startOfMonth(new Date()),
- endDate: endOfMonth(new Date())
- },
- {
- label: 'Last Month',
- startDate: startOfMonth(addMonths(new Date(), -1)),
- endDate: endOfMonth(addMonths(new Date(), -1))
- },
- {
- label: 'All',
- startDate: new Date(),
- endDate: addDays(new Date(), -1)
- }
+ { label: 'Today', startDate: new Date(), endDate: new Date() },
+ { label: 'Yesterday', startDate: addDays(new Date(), -1), endDate: addDays(new Date(), -1) },
+ { label: 'Tomorrow', startDate: addDays(new Date(), +1), endDate: addDays(new Date(), +1) },
+ { label: 'This Week', startDate: startOfWeek(new Date()), endDate: endOfWeek(new Date()) },
+ { label: 'Last Week', startDate: startOfWeek(addWeeks(new Date(), -1)), endDate: endOfWeek(addWeeks(new Date(), -1)) },
+ { label: 'Last 7 Days', startDate: addWeeks(new Date(), -1), endDate: new Date() },
+ { label: 'This Month', startDate: startOfMonth(new Date()), endDate: endOfMonth(new Date()) },
+ { label: 'Last Month', startDate: startOfMonth(addMonths(new Date(), -1)), endDate: endOfMonth(addMonths(new Date(), -1)) },
+ { label: 'All', startDate: new Date(), endDate: addDays(new Date(), -1) }
]}
/>
-
- setOpen(false)}>
- ok
+
+ setOpen(false)}
+ sx={{
+ borderRadius: 999,
+ px: 2.5,
+ borderColor: DT.borderSubtle,
+ color: DT.textSecondary,
+ fontWeight: 700,
+ '&:hover': { borderColor: DT.textSecondary, bgcolor: DT.surfaceAlt }
+ }}
+ >
+ Cancel
+
+ setOpen(false)}
+ sx={{
+ borderRadius: 999,
+ px: 3,
+ bgcolor: BRAND,
+ fontWeight: 700,
+ boxShadow: `0 6px 18px ${ring(BRAND)}`,
+ '&:hover': { bgcolor: '#4D1C61' }
+ }}
+ >
+ Apply
diff --git a/src/routes/MainRoutes.js b/src/routes/MainRoutes.js
index 3be5961..49760ca 100644
--- a/src/routes/MainRoutes.js
+++ b/src/routes/MainRoutes.js
@@ -40,6 +40,9 @@ const RiderLogs = Loadable(lazy(() => import('pages/nearle/reports/riderLogs')))
const Invoice = Loadable(lazy(() => import('pages/nearle/invoice/invoice')));
const InvoicePreview = Loadable(lazy(() => import('../pages/nearle/invoice/invoicePreview')));
+const Dispatch = Loadable(lazy(() => import('pages/nearle/dispatch/Dispatch')));
+const DispatchPreview = Loadable(lazy(() => import('pages/nearle/dispatch/Preview')));
+
// ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = {
@@ -123,6 +126,19 @@ const MainRoutes = {
{
path: 'locations',
element:
+ },
+ {
+ path: 'dispatch',
+ children: [
+ {
+ path: '',
+ element:
+ },
+ {
+ path: 'preview',
+ element:
+ }
+ ]
}
]
},
diff --git a/src/utils/leafletPolylineOffset.js b/src/utils/leafletPolylineOffset.js
new file mode 100644
index 0000000..2e386e8
--- /dev/null
+++ b/src/utils/leafletPolylineOffset.js
@@ -0,0 +1,154 @@
+// Vendored from leaflet-polylineoffset@1.1.1 (MIT).
+//
+// Why this lives in-tree instead of being an npm dep:
+// • The published package would require --legacy-peer-deps because of an
+// unrelated React-17 peer-dep conflict elsewhere in the project, and we
+// don't want a renderer plugin to force a global resolver flag.
+// • It's frozen upstream (no meaningful updates since 2020), tiny, and
+// has zero runtime deps besides leaflet (already in package.json).
+//
+// What it does:
+// Monkey-patches L.Polyline so that any path passed with a numeric
+// `offset` in pathOptions is rendered shifted perpendicular to its
+// direction of travel by that many pixels (positive = right of travel,
+// negative = left). Used by Dispatch.js's Compare → Combined view to
+// render planned + actual as parallel rails when they share the same
+// road geometry; without this they overlap and read as one polyline.
+//
+// Import once for the side effect:
+// import '../../../utils/leafletPolylineOffset';
+//
+// Then add to any pathOptions:
+// pathOptions={{ ..., offset: 5 }}
+//
+// Plays nicely with both SVG and Canvas renderers.
+
+import L from 'leaflet';
+
+L.PolylineOffset = {
+ translatePoint(pt, dist, radians) {
+ return L.point(pt.x + dist * Math.cos(radians), pt.y + dist * Math.sin(radians));
+ },
+
+ offsetPointLine(points, distance) {
+ const l = points.length;
+ if (l < 2) {
+ throw new Error('Line should be defined by at least 2 points');
+ }
+ let a = points[0];
+ let b;
+ const offsetAngle = Math.PI / 2;
+ const offsetSegments = [];
+
+ for (let i = 1; i < l; i++) {
+ b = points[i];
+ // Each segment's offset angle is perpendicular to its direction.
+ const segAngle = Math.atan2(b.y - a.y, b.x - a.x);
+ offsetSegments.push({
+ offsetAngle: segAngle - offsetAngle,
+ original: [a, b],
+ offset: [
+ this.translatePoint(a, distance, segAngle - offsetAngle),
+ this.translatePoint(b, distance, segAngle - offsetAngle)
+ ]
+ });
+ a = b;
+ }
+
+ return offsetSegments;
+ },
+
+ // Find the intersection of two segments by extending them to infinity
+ // along their direction, then walking along segment 1 by parameter t.
+ // Returns null when the segments are parallel (no intersection).
+ intersection(l1a, l1b, l2a, l2b) {
+ const line1 = this.segmentAsVector(l1a, l1b);
+ const line2 = this.segmentAsVector(l2a, l2b);
+
+ const denom = -line2.x * line1.y + line1.x * line2.y;
+ if (denom === 0) return null;
+
+ const s = (-line1.y * (l1a.x - l2a.x) + line1.x * (l1a.y - l2a.y)) / denom;
+ const t = (line2.x * (l1a.y - l2a.y) - line2.y * (l1a.x - l2a.x)) / denom;
+
+ if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
+ return L.point(l1a.x + t * line1.x, l1a.y + t * line1.y);
+ }
+ return null;
+ },
+
+ segmentAsVector(a, b) {
+ return L.point(b.x - a.x, b.y - a.y);
+ },
+
+ // Walk the offset segments and join adjacent ones at their intersection
+ // points (mitered corners). When two consecutive segments don't intersect
+ // within their bounds (sharp turn, or co-linear), fall back to the offset
+ // endpoint so the polyline doesn't gap.
+ joinLineSegments(segments) {
+ const joined = [];
+ let last = segments[0].offset;
+ joined.push(last[0]);
+
+ for (let i = 1; i < segments.length; i++) {
+ const next = segments[i].offset;
+ const inter = this.intersection(last[0], last[1], next[0], next[1]);
+ if (inter) {
+ joined.push(inter);
+ } else {
+ joined.push(last[1]);
+ }
+ last = next;
+ }
+ joined.push(last[1]);
+ return joined;
+ },
+
+ offsetPoints(points, offset) {
+ if (!points || points.length < 2) return points;
+ const offsets = this.offsetPointLine(points, offset);
+ return this.joinLineSegments(offsets);
+ },
+
+ // Operates on a ring of LatLngs by projecting → offsetting → unprojecting,
+ // since leaflet polyline math is in screen pixels but our points are LatLng.
+ offsetLatLngs(map, latlngs, offset) {
+ const points = latlngs.map((ll) => map.latLngToLayerPoint(ll));
+ const offsetPts = this.offsetPoints(points, offset);
+ return offsetPts.map((p) => map.layerPointToLatLng(p));
+ }
+};
+
+// Patch Polyline._projectLatlngs (used by both SVG and Canvas renderers) so
+// that when an offset is set, the projected ring is offset before clipping.
+// We keep the original on _projectLatlngsOriginal so we can call through.
+const originalProject = L.Polyline.prototype._projectLatlngs;
+
+L.Polyline.prototype._projectLatlngs = function patchedProject(latlngs, result, projectedBounds) {
+ const offset = this.options.offset;
+ if (!offset || typeof offset !== 'number') {
+ return originalProject.call(this, latlngs, result, projectedBounds);
+ }
+
+ // Recurse for multi-ring polylines (shouldn't happen for simple lines,
+ // but the leaflet API allows it).
+ const flat = latlngs[0] instanceof L.LatLng;
+ if (!flat) {
+ for (let i = 0; i < latlngs.length; i++) {
+ this._projectLatlngs(latlngs[i], result, projectedBounds);
+ }
+ return undefined;
+ }
+
+ const projected = latlngs.map((ll) => this._map.latLngToLayerPoint(ll));
+ const offsetted = L.PolylineOffset.offsetPoints(projected, offset);
+ // Update projectedBounds with each offset point so the renderer's
+ // viewport-clipping check still works.
+ for (let i = 0; i < offsetted.length; i++) {
+ projectedBounds.extend(offsetted[i]);
+ }
+ result.push(offsetted);
+ return undefined;
+};
+
+export default L;
diff --git a/src/utils/logger.js b/src/utils/logger.js
new file mode 100644
index 0000000..3da46c0
--- /dev/null
+++ b/src/utils/logger.js
@@ -0,0 +1,66 @@
+const LOG_LEVELS = {
+ DEBUG: 0,
+ INFO: 1,
+ WARN: 2,
+ ERROR: 3,
+};
+
+// Default log level based on environment
+const currentEnv = process.env.NODE_ENV || 'development';
+const isDev = currentEnv === 'development';
+const GLOBAL_LOG_LEVEL = isDev ? LOG_LEVELS.DEBUG : LOG_LEVELS.WARN;
+
+const style = (bg, color) => `background: ${bg}; color: ${color}; padding: 2px 5px; border-radius: 4px; font-weight: bold;`;
+
+const PREFIX = '%c[NearlExpress]';
+const PREFIX_STYLE = style('#2563eb', '#ffffff');
+
+// Capture original console methods before any global overrides occur
+const originalLog = console.log;
+const originalWarn = console.warn || console.log;
+const originalError = console.error || console.log;
+
+const print = (levelName, args, labelStyle) => {
+ const levelValue = LOG_LEVELS[levelName];
+ if (levelValue < GLOBAL_LOG_LEVEL) return;
+
+ const [message, ...extra] = args;
+ const isMessageString = typeof message === 'string';
+
+ const formatPrefix = `${PREFIX}%c ${levelName}`;
+ const styles = [PREFIX_STYLE, labelStyle];
+
+ const consoleMethod =
+ levelName === 'ERROR' ? originalError :
+ levelName === 'WARN' ? originalWarn :
+ originalLog;
+
+ if (isMessageString) {
+ consoleMethod(
+ `${formatPrefix}%c ${message}`,
+ ...styles,
+ 'color: inherit;',
+ ...extra
+ );
+ } else {
+ // If first argument is an object/array, preserve raw interactive log
+ consoleMethod(
+ `${formatPrefix}`,
+ ...styles,
+ message,
+ ...extra
+ );
+ }
+};
+
+const logger = {
+ debug: (...args) => print('DEBUG', args, style('#64748b', '#ffffff')),
+ info: (...args) => print('INFO', args, style('#10b981', '#ffffff')),
+ warn: (...args) => print('WARN', args, style('#f59e0b', '#ffffff')),
+ error: (...args) => {
+ print('ERROR', args, style('#ef4444', '#ffffff'));
+ // Future expansion hook: e.g., Sentry.captureException(args[0]);
+ },
+};
+
+export default logger;
diff --git a/yarn.lock b/yarn.lock
index f4320fb..7189b30 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5795,11 +5795,6 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
-fsevents@^2.3.2, fsevents@~2.3.2:
- version "2.3.2"
- resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz"
- integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
-
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"