From 8d0c796ba592b1d192b308ffbf676c818d97de22 Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Tue, 2 Jun 2026 13:09:29 +0530 Subject: [PATCH] updates on the dispatch page and redesigned the maximum pages --- .gitignore | 2 +- src/components/third-party/ReactTable.js | 26 +- src/config.js | 2 +- src/menu-items/nearle.js | 8 + src/pages/nearle/api/api.js | 129 +- src/pages/nearle/dispatch/CompareDataPanel.js | 976 ++ src/pages/nearle/dispatch/Dispatch.css | 9848 +++++++++++++++++ src/pages/nearle/dispatch/Dispatch.js | 5445 +++++++++ src/pages/nearle/dispatch/Preview.js | 806 ++ src/pages/nearle/dispatch/dispatchShared.js | 65 + src/pages/nearle/invoice/invoice.js | 1013 +- .../locations/ResponsiveLocationDrawer.js | 21 +- src/pages/nearle/login.js | 4 +- src/pages/nearle/orders/OrdersRedesign.css | 288 + src/pages/nearle/orders/createorder1.js | 2290 ++-- src/pages/nearle/orders/details.js | 725 +- src/pages/nearle/orders/orders.js | 1488 ++- src/pages/nearle/reports/orderSummary.js | 1498 +-- src/pages/nearle/reports/ordersDetails.js | 1562 ++- src/pages/nearle/reports/riderLogs.js | 761 +- src/pages/nearle/reports/ridersummary.js | 1340 ++- src/routes/MainRoutes.js | 16 + src/utils/leafletPolylineOffset.js | 154 + src/utils/logger.js | 66 + yarn.lock | 5 - 25 files changed, 24405 insertions(+), 4133 deletions(-) create mode 100644 src/pages/nearle/dispatch/CompareDataPanel.js create mode 100644 src/pages/nearle/dispatch/Dispatch.css create mode 100644 src/pages/nearle/dispatch/Dispatch.js create mode 100644 src/pages/nearle/dispatch/Preview.js create mode 100644 src/pages/nearle/dispatch/dispatchShared.js create mode 100644 src/pages/nearle/orders/OrdersRedesign.css create mode 100644 src/utils/leafletPolylineOffset.js create mode 100644 src/utils/logger.js 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 ( - - + + ); }; +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 + {locationMenuOpen && ( +
    + {appLocations.map((loc) => { + const isActive = String(loc.applocationid) === String(selectedAppLocationId); + return ( + + ); + })} +
    + )} + + )} + + + {/* Header right-cluster: total-orders pill, date picker. Sits to the + LEFT of the running clock so the operator sees current wave size + + selected date together in one row. */} +
    + {shouldFetchLive && ( + <> + {liveIsFetching && ( + + Loading {liveRows.length ? `· ${liveRows.length} loaded` : ''} + + )} + {!liveIsFetching && !liveIsError && ( + + {filteredLiveRows.length} orders + / {liveRows.length} total + + )} + {liveIsError && ( + + Failed to load + + )} + {(() => { + // Date-picker chip + custom calendar popover. Replaces the + // OS-native dialog (which looks different + // on every browser and can't pick up the design system) with + // a single popover that always renders the same way. The + // chip has three regions: + // • prev-day arrow — one-click ±1 day scrubbing + // • center card — opens the calendar popover on click + // • next-day arrow — disabled when viewing today + // The popover itself carries the month grid + quick presets. + const today = dayjs().startOf('day'); + const todayStr = today.format('YYYY-MM-DD'); + const picked = dayjs(selectedDate); + const isToday = selectedDate === todayStr; + const isFuture = picked.isAfter(today, 'day'); + + const commitDate = (next) => { + if (!next) return; + const str = next.format('YYYY-MM-DD'); + if (str === selectedDate) { + setDatePickerOpen(false); + return; + } + if (next.isAfter(today, 'day')) return; // guard future + setSelectedDate(str); + handleRiderFocus(null); + setFocusedKitchen(null); + setFocusedZone(null); + setDatePickerOpen(false); + }; + + const goPrevDay = () => commitDate(picked.subtract(1, 'day')); + const goNextDay = () => { + if (isToday || isFuture) return; + commitDate(picked.add(1, 'day')); + }; + + // Build a fixed 6×7 grid of dayjs instances starting from + // the Sunday before the month's first day. Days outside the + // visible month render as faded "other-month" cells so the + // grid never jumps in height as months change. + const monthStart = calViewMonth.startOf('month'); + const gridStart = monthStart.subtract(monthStart.day(), 'day'); + const cells = Array.from({ length: 42 }, (_, i) => gridStart.add(i, 'day')); + + const prevMonth = () => setCalViewMonth((m) => m.subtract(1, 'month')); + const nextMonth = () => { + const candidate = calViewMonth.add(1, 'month'); + // Allow navigating into the current month (so "today" can + // be reached) but not into purely-future months. + if (candidate.startOf('month').isAfter(today, 'month')) return; + setCalViewMonth(candidate); + }; + const canGoNextMonth = !calViewMonth.add(1, 'month').startOf('month').isAfter(today, 'month'); + + const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + + return ( +
    + + + + + {datePickerOpen && ( +
    + {/* Month header — month/year title flanked by + prev/next arrows. The next-month arrow disables + once we'd cross into a purely-future month so + the operator never lands on a month they can't + pick a date in. */} +
    + +
    + {calViewMonth.format('MMMM YYYY')} +
    + +
    + +
    + {WEEKDAYS.map((w) => ( +
    {w}
    + ))} +
    + +
    + {cells.map((d) => { + const inMonth = d.month() === calViewMonth.month(); + const isSel = d.format('YYYY-MM-DD') === selectedDate; + const isTodayCell = d.format('YYYY-MM-DD') === todayStr; + const disabled = d.isAfter(today, 'day'); + const cls = [ + 'date-cal-day', + !inMonth && 'is-other-month', + isSel && 'is-selected', + isTodayCell && 'is-today', + disabled && 'is-disabled' + ].filter(Boolean).join(' '); + return ( + + ); + })} +
    + + {/* Quick presets — the three dates ops scrub to + most often. Saves a month-nav + a day-click for + the common cases. */} +
    + + + +
    +
    + )} +
    + ); + })()} + + )} +
    + +
    {clock}
    + + )} + + {(embedded || topView === 'live') && (<> +
    + + + + + +
    + + {shouldFetchLive && viewMode !== 'rider-info' && ( +
    + Batch + {/* Status-wise (time-field) filter is hidden for now per spec — + bucketing is locked to `assigntime`. Restore this block to bring + back the Delivered/Pending/Assigned/... dropdown. +
    + + {timeFieldMenuOpen && ( +
    + {TIME_FIELDS.map((f) => { + const isActive = f.id === selectedTimeField; + return ( + + ); + })} +
    + )} +
    + */} + {/* Slot editor (Edit slots button + panel) is hidden for now per + spec — the three batches (Morning / Afternoon / Evening) are + fixed. Restore this block to bring back the operator-editable + start/end hours, add-slot, and reset-to-defaults controls. +
    + + {slotEditOpen && ( +
    +
    +
    Slot timings
    +
    Hours are 0–24 (24h clock). Half-hour steps allowed (e.g. 12.5 = 12:30). Start < End.
    +
    +
    + {slotsConfig.map((s, idx) => ( +
    + {idx + 1} + + + + {formatSlotRange(s.startHour, s.endHour)} + + +
    + ))} +
    +
    + + +
    +
    + )} +
    + */} + {/* Inner scroller — keeps the "Slot" label fixed while the chip list scrolls + horizontally when it overflows. */} +
    + {BATCHES.map((b) => { + const isActive = selectedBatch === b.id; + return ( + + ); + })} +
    +
    + )} + + {viewMode === 'rider-info' ? ( +
    +
    +
    +
    Riders
    +
    {ridersAllDay.length} {ridersAllDay.length === 1 ? 'rider' : 'riders'} today
    +
    +
    + + setRiderInfoSearch(e.target.value)} + /> +
    + {(() => { + const q = riderInfoSearch.trim().toLowerCase(); + const matched = ridersAllDay.filter((r) => { + if (!q) return true; + return String(r.riderName || '').toLowerCase().includes(q) || String(r.id).includes(q); + }); + if (matched.length === 0) { + return
    {riderInfoSearch ? `No riders match "${riderInfoSearch}"` : 'No riders have orders today'}
    ; + } + return ( +
    + {matched.map((r) => { + const isActive = String(riderInfoUserid) === String(r.id); + return ( + + ); + })} +
    + ); + })()} +
    + +
    + {riderInfoUserid == null ? ( +
    +
    +
    Pick a rider
    +
    + Select a rider from the list on the left to see their live GPS, + battery, connection, and current order snapshot. +
    +
    + ) : ( + <> + {riderInfoFetching && !riderInfoData && ( +
    Loading rider snapshot…
    + )} + + {riderInfoIsError && ( +
    + Couldn't load this rider's log. {riderInfoError?.message || ''} +
    + )} + + {riderInfoData && (() => { + const d = riderInfoData; + const lat = parseFloat(d.latitude); + const lon = parseFloat(d.longitude); + const hasCoords = Number.isFinite(lat) && Number.isFinite(lon); + const batteryNum = parseInt(String(d.battery || '').replace('%', ''), 10); + const batteryLow = Number.isFinite(batteryNum) && batteryNum <= 20; + const speedNum = parseFloat(d.speed); + const statusKey = String(d.status || '').toLowerCase(); + return ( +
    +
    +
    {d.username || `Rider #${d.userid}`}
    +
    + #{d.userid} + {d.status && ( + {d.status} + )} + + + {riderInfoFetching ? 'Updating…' : 'Live'} + +
    + {d.logdate && ( +
    + Last seen {d.logdate} +
    + )} +
    + +
    +
    +
    +
    +
    Battery
    +
    + {d.battery || '—'} + {d.is_charging && Charging} +
    +
    +
    + +
    +
    +
    +
    Connection
    +
    {d.connection || '—'}
    +
    +
    + +
    +
    +
    +
    GPS Accuracy
    +
    {d.accuracy ? `${d.accuracy} m` : '—'}
    +
    +
    + +
    +
    +
    +
    Location Service
    +
    {d.location_service || '—'}
    +
    +
    + +
    +
    +
    +
    Speed
    +
    + {Number.isFinite(speedNum) ? `${speedNum.toFixed(2)} km/h` : '—'} +
    +
    +
    + +
    +
    +
    +
    Heading
    +
    {d.heading != null ? `${d.heading}°` : '—'}
    +
    +
    + +
    +
    +
    +
    App State
    +
    {d.is_background ? 'Background' : 'Foreground'}
    +
    +
    + +
    +
    +
    +
    Current Order
    +
    {d.orderid || '—'}
    +
    +
    +
    + + {hasCoords && ( +
    +
    + {lat.toFixed(6)}, {lon.toFixed(6)} +
    +
    + {/* `key` forces a remount when the rider changes so the + MapContainer re-centers on the new coords (leaflet's + center prop is only read on mount). */} + + + + {/* Permanent banner above the pin — Nominatim + reverse-geocode tells the operator which + suburb/area the rider is in. Falls back to + a "Locating…" hint while the request is in + flight so the pin never looks unlabeled. */} + + {riderInfoArea?.area || 'Locating area…'} + + +
    {d.username || `Rider #${d.userid}`}
    + {riderInfoArea?.area && ( +
    + {riderInfoArea.area} +
    + )} +
    + {d.logdate ? `Last seen ${d.logdate}` : `${lat.toFixed(6)}, ${lon.toFixed(6)}`} +
    +
    +
    +
    +
    +
    + )} +
    + ); + })()} + + )} +
    +
    + ) : ( +
    + + {compareOpen && focusedRider && ( + + )} + + ); +}; + +export default Dispatch; diff --git a/src/pages/nearle/dispatch/Preview.js b/src/pages/nearle/dispatch/Preview.js new file mode 100644 index 0000000..360b900 --- /dev/null +++ b/src/pages/nearle/dispatch/Preview.js @@ -0,0 +1,806 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { + Autocomplete, + Backdrop, + Box, + Button, + Card, + Chip, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + IconButton, + Stack, + Tab, + Tabs, + TextField, + Tooltip, + Typography +} from '@mui/material'; +import { useMutation, useQuery } from '@tanstack/react-query'; +import dayjs from 'dayjs'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import { HiOutlineArrowLeft } from 'react-icons/hi'; +import { IoReload } from 'react-icons/io5'; +import { MdTwoWheeler, MdSwapHoriz } from 'react-icons/md'; + +import { + createAutomationDeliveries, + createOptimisationDeliveries, + fetchRidersList, + finalCreatedeliveries, + notifyRider, + reconcileSteps +} from '../api/api'; +import { OpenToast } from 'components/nearle_components/OpenToast'; +import CSVExport from 'components/third-party/ReactTable'; +import CircularLoader from 'components/nearle_components/CircularLoader'; +import Dispatch from './Dispatch'; +import { stepColor } from './dispatchShared'; + +const tuningTypes = [ + { tuneid: 1, type: 'Balanced', value: 'balanced' }, + { tuneid: 2, type: 'Aggressive Speed', value: 'aggressive_speed' }, + { tuneid: 3, type: 'Fuel Saver', value: 'fuel_saver' }, + { tuneid: 4, type: 'Zone Strict', value: 'zone_strict' } +]; + +// Flatten the API's zoned shape into [{ rider_id, rider_name, orders }] for +// the Reconcile tab UI and the reconcile-API payload. +const extractRiders = (previewData) => { + if (!previewData) return []; + const map = new Map(); + // De-dupe by orderid across the whole tree. A rider can legitimately appear + // in multiple zones (one per delivery suburb), so the same rider_id is + // visited more than once. Without this guard, any stale copy left behind + // by applyReconcileResponse gets concatenated into the rider's orders and + // the same orderid is sent twice to /deliveries/createdeliveries. + const seenOrderIds = new Set(); + const push = (riderId, riderName, orders) => { + if (riderId == null) return; + const key = String(riderId); + if (!map.has(key)) { + map.set(key, { rider_id: riderId, rider_name: riderName, orders: [] }); + } + const entry = map.get(key); + (orders || []).forEach((o) => { + const oid = o?.orderid != null ? String(o.orderid) : null; + if (oid) { + if (seenOrderIds.has(oid)) return; + seenOrderIds.add(oid); + } + entry.orders.push(o); + }); + if (!entry.rider_name && riderName) entry.rider_name = riderName; + }; + + if (Array.isArray(previewData.zones) && previewData.zones.length) { + previewData.zones.forEach((z) => { + (z.riders || []).forEach((r) => { + const id = r.rider_id ?? r.userid; + const name = r.rider_name || r.username || `Rider ${id}`; + push(id, name, r.orders); + }); + }); + } else if (Array.isArray(previewData.details)) { + previewData.details.forEach((o) => { + const id = o.rider_id ?? o.userid; + const name = o.rider_name || o.ridername || `Rider ${id}`; + push(id, name, [o]); + }); + } + return Array.from(map.values()); +}; + +// Reverse of extractRiders — flatten rider-grouped list into a details-style +// array (used as the Assign Orders payload). +const flattenRiders = (riders) => { + const out = []; + riders.forEach((r) => { + // Go backend types Deliveries.userid as int — coerce here so any + // upstream string (AI response, riders API, change-rider edit) gets + // normalised before the JSON body is built. + const ridNum = Number(r.rider_id); + const rid = Number.isFinite(ridNum) ? ridNum : r.rider_id; + (r.orders || []).forEach((o) => { + out.push({ + ...o, + rider_id: rid, + userid: rid, + rider_name: r.rider_name, + rider: r.rider_name + }); + }); + }); + return out; +}; + +// Move one order from oldRiderId -> newRiderId inside dispatchPreviewData. +// Mutates both the zones[].riders[].orders[] tree (so the Dispatch tab +// renders the change) AND the flat details[] list (so Assign Orders picks +// it up). Returns a NEW preview object (immutable update). +const moveOrderInPreviewData = (preview, { orderId, newRiderId, newRiderName }) => { + if (!preview) return preview; + const next = JSON.parse(JSON.stringify(preview)); + + // 1) Update flat details list + if (Array.isArray(next.details)) { + next.details = next.details.map((o) => + String(o.orderid) === String(orderId) + ? { ...o, rider_id: newRiderId, userid: newRiderId, rider_name: newRiderName, rider: newRiderName } + : o + ); + } + + // 2) Move within zones[].riders[].orders[] + if (Array.isArray(next.zones)) { + let movedOrder = null; + let homeZoneIdx = -1; + + for (let zi = 0; zi < next.zones.length && !movedOrder; zi++) { + const zone = next.zones[zi]; + if (!Array.isArray(zone.riders)) continue; + for (let ri = 0; ri < zone.riders.length && !movedOrder; ri++) { + const r = zone.riders[ri]; + if (!Array.isArray(r.orders)) continue; + const oi = r.orders.findIndex((o) => String(o.orderid) === String(orderId)); + if (oi !== -1) { + movedOrder = r.orders[oi]; + r.orders.splice(oi, 1); + homeZoneIdx = zi; + } + } + } + + if (movedOrder) { + const updated = { + ...movedOrder, + rider_id: newRiderId, + userid: newRiderId, + rider_name: newRiderName, + rider: newRiderName + }; + let placed = false; + for (const zone of next.zones) { + if (!Array.isArray(zone.riders)) continue; + const target = zone.riders.find( + (r) => String(r.rider_id ?? r.userid) === String(newRiderId) + ); + if (target) { + target.orders = target.orders || []; + target.orders.push(updated); + placed = true; + break; + } + } + if (!placed && homeZoneIdx >= 0) { + next.zones[homeZoneIdx].riders.push({ + rider_id: newRiderId, + userid: newRiderId, + rider_name: newRiderName, + orders: [updated] + }); + } + } + } + + return next; +}; + +// Merge a reconcile-API response { riders:[{rider_id, orders}] } back into +// dispatchPreviewData. Replaces each rider's orders[] in zones (preserving +// zone containment), then rebuilds the flat details list from the new tree. +const applyReconcileResponse = (preview, response) => { + if (!preview || !Array.isArray(response?.riders)) return preview; + const next = JSON.parse(JSON.stringify(preview)); + + const newOrdersByRider = new Map( + response.riders.map((r) => [String(r.rider_id), r.orders || []]) + ); + + if (Array.isArray(next.zones) && next.zones.length) { + // Pass 1: wipe every existing copy of a responding rider's orders across + // ALL zones. The server's reconciled list is the single source of truth, + // and a rider can be present in multiple zones (one per delivery suburb). + // The previous "update first match, delete from map" loop left stale + // copies in the other zones, which extractRiders then concatenated into + // duplicate orderids — surfacing as duplicate deliveries on Assign. + next.zones.forEach((zone) => { + if (!Array.isArray(zone.riders)) return; + zone.riders.forEach((r) => { + const key = String(r.rider_id ?? r.userid); + if (newOrdersByRider.has(key)) r.orders = []; + }); + }); + + // Pass 2: drop the reconciled orders onto the first zone that already + // lists the rider. If the rider isn't anywhere in the tree, append a + // fresh rider entry to zone[0]. + newOrdersByRider.forEach((orders, riderKey) => { + let placed = false; + for (const zone of next.zones) { + if (!Array.isArray(zone.riders)) continue; + const target = zone.riders.find( + (r) => String(r.rider_id ?? r.userid) === riderKey + ); + if (target) { + target.orders = orders; + placed = true; + break; + } + } + if (!placed) { + const target = next.zones[0]; + target.riders = target.riders || []; + target.riders.push({ + rider_id: Number(riderKey) || riderKey, + rider_name: orders[0]?.rider_name || `Rider ${riderKey}`, + orders + }); + } + }); + } else { + next.zones = [ + { + zone_name: 'Reconciled', + riders: response.riders.map((r) => ({ + rider_id: r.rider_id, + rider_name: r.rider_name || `Rider ${r.rider_id}`, + orders: r.orders || [] + })) + } + ]; + } + + // Rebuild flat details from the updated zones->riders->orders tree. + const flatDetails = []; + next.zones.forEach((zone) => { + (zone.riders || []).forEach((r) => { + (r.orders || []).forEach((o) => { + flatDetails.push({ + ...o, + rider_id: r.rider_id, + userid: r.rider_id, + rider_name: r.rider_name, + rider: r.rider_name + }); + }); + }); + }); + next.details = flatDetails; + + return next; +}; + +const Preview = () => { + const navigate = useNavigate(); + const location = useLocation(); + const stateData = location.state || {}; + + // SINGLE SOURCE OF TRUTH: every Change Rider / Reconcile / Re-Assign goes + // through this state. The Dispatch tab renders from it, the Reconcile tab + // derives its rider list from it, and Assign Orders sends a flattened copy + // of it to the API. + const [dispatchPreviewData, setDispatchPreviewData] = useState(stateData.dispatchPreviewData || null); + + // The AI response arrives via location.state, which the browser stores in + // history.state and persists across reloads. That means a reload of + // /dispatch/preview would re-hydrate the stale snapshot — including any + // pending edits the user thought they discarded. Bounce to /orders when + // there's no fresh response, and wipe the history snapshot once consumed + // so a later reload / back-forward also bounces instead of re-using it. + useEffect(() => { + if (!stateData.dispatchPreviewData) { + navigate('/nearle/orders', { replace: true }); + return; + } + if (typeof window !== 'undefined' && window.history?.state) { + window.history.replaceState({ ...window.history.state, usr: null }, ''); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const [csvExportData, setCsvExportData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [tabValue, setTabValue] = useState(0); + + const [reconcileLoading, setReconcileLoading] = useState(false); + const [hasReconciled, setHasReconciled] = useState(false); + + // Tracks riders whose orders have been edited since the last AI response + // or successful reconcile. Only these are sent to the reconcile API — the + // server-side step re-ordering only needs to see what actually changed. + const [dirtyRiderIds, setDirtyRiderIds] = useState(() => new Set()); + + // Change-rider dialog state + const [changeDialogOpen, setChangeDialogOpen] = useState(false); + const [selectedOrder, setSelectedOrder] = useState(null); + const [selectedOldRiderId, setSelectedOldRiderId] = useState(null); + const [selectedNewRider, setSelectedNewRider] = useState(null); + + const aiMode = stateData.aiMode ?? 1; + const selectedMode = stateData.selectedMode || null; + const deliveryData = stateData.deliveryData || []; + const autoRiders = stateData.autoRiders || []; + const absentRidersPayload = stateData.absentRidersPayload || []; + const rider = stateData.rider || null; + + const appId = useMemo(() => { + if (stateData.appId) return stateData.appId; + if (typeof window !== 'undefined') { + const v = localStorage.getItem('applocationid'); + return v ? Number(v) : 0; + } + return 0; + }, [stateData.appId]); + + const { data: ridersList } = useQuery({ + queryKey: ['ridersList', appId], + queryFn: fetchRidersList, + enabled: !!appId, + staleTime: 5 * 60 * 1000 + }); + + // Derived: rider list for the Reconcile tab. Recomputes whenever the cache + // (dispatchPreviewData) changes — so Change Rider / Reconcile both reflect + // here without a separate state. + const reconcileRiders = useMemo(() => extractRiders(dispatchPreviewData), [dispatchPreviewData]); + + // Derived: flat orders list used for the Assign Orders payload + CSV export. + // Always reflects the latest cache state. + const finaldeliveryList = useMemo(() => { + const flat = flattenRiders(reconcileRiders); + if (flat.length) return computeDeliveryAmounts(flat); + if (Array.isArray(dispatchPreviewData?.details)) { + return computeDeliveryAmounts(dispatchPreviewData.details); + } + return []; + }, [reconcileRiders, dispatchPreviewData]); + + useEffect(() => { + const filtered = finaldeliveryList.map((item) => ({ + zone_name: item.zone_name, + ordernotes: item.ordernotes, + rider: item.rider, + step: item.step, + ordertype: item.ordertype, + orderamount: item.orderamount, + riderkms: item.riderkms, + cumulativekms: item.cumulativekms, + baseprice: item.baseprice, + minkm: item.minkm, + priceperkm: item.priceperkm, + kms: item.kms, + actualkms: item.actualkms, + rider_charge: item.rider_charge, + deliveryamt: item.deliveryamt, + deliverycharges: item.deliverycharges, + profit: item.profit + })); + setCsvExportData(filtered); + }, [finaldeliveryList]); + + const notifyRiderMutation = useMutation({ + mutationFn: notifyRider, + onSuccess: () => OpenToast('Notification sent Successfully', 'success', 2000), + onError: (error) => OpenToast(error.message, 'error', 2000) + }); + + const createDeliveryMutation = useMutation({ + mutationFn: aiMode == 0 ? createOptimisationDeliveries : createAutomationDeliveries, + onSuccess: (data) => { + OpenToast('Orders Optimised Successfully', 'success', 2000); + // Brand new response = brand new source of truth. + setDispatchPreviewData(data); + setHasReconciled(false); + setDirtyRiderIds(new Set()); + setIsLoading(false); + }, + onError: (error) => { + OpenToast(error.message, 'error', 4000); + setIsLoading(false); + }, + onSettled: () => setIsLoading(false) + }); + + const createFinalDeliveryMutation = useMutation({ + mutationFn: finalCreatedeliveries, + onSuccess: () => { + OpenToast('Delivery Created Successfully', 'success', 2000); + setIsLoading(false); + if (rider?.userfcmtoken) notifyRiderMutation.mutate(rider.userfcmtoken); + navigate('/nearle/deliveries'); + }, + onError: (error) => { + OpenToast(error.message, 'error', 4000); + setIsLoading(false); + }, + onSettled: () => setIsLoading(false) + }); + + const reconcileMutation = useMutation({ + mutationFn: reconcileSteps, + onMutate: () => setReconcileLoading(true), + onSuccess: (data) => { + if (Array.isArray(data?.riders)) { + // Merge: applyReconcileResponse replaces orders for riders present + // in the response and leaves the rest of the cache untouched. + setDispatchPreviewData((prev) => applyReconcileResponse(prev, data)); + setHasReconciled(true); + // Clear only the riders we just reconciled from the dirty set, so + // any unrelated edits made meanwhile are preserved. + setDirtyRiderIds((prev) => { + const next = new Set(prev); + data.riders.forEach((r) => next.delete(String(r.rider_id))); + return next; + }); + OpenToast('Steps reconciled — preview updated', 'success', 2000); + } else { + OpenToast('Reconcile returned no rider data', 'warning', 3000); + } + }, + onError: (error) => { + OpenToast(error.message || 'Reconcile failed', 'error', 4000); + }, + onSettled: () => setReconcileLoading(false) + }); + + const handleCreateDelivery = (tune) => { + setIsLoading(true); + if (aiMode == 0) { + createDeliveryMutation.mutate({ deliveries: deliveryData }); + } else if (selectedMode && selectedMode?.value == 1) { + createDeliveryMutation.mutate({ + deliveries: deliveryData, + hypertuning_params: tune || null, + selectedMode, + absent_riders: absentRidersPayload + }); + } else { + createDeliveryMutation.mutate({ + data: { + orders: deliveryData, + riders: autoRiders, + config: { pay_type: 'hourly', base_pay: 300.0, strategy: 'multi_trip' }, + absent_riders: absentRidersPayload + }, + selectedMode + }); + } + }; + + const handleFinalCreateDelivery = () => { + if (!finaldeliveryList?.length) { + OpenToast('No deliveries to assign', 'error', 3000); + return; + } + setIsLoading(true); + createFinalDeliveryMutation.mutate({ deliveries: finaldeliveryList }); + }; + + const handleReconcile = () => { + if (!reconcileRiders.length) { + OpenToast('No riders to reconcile', 'warning', 3000); + return; + } + // Only send riders that were edited since the last AI response / reconcile. + // Their step ordering is the only thing that can be stale — untouched + // riders are skipped to keep the payload small. + const dirty = reconcileRiders.filter((r) => + dirtyRiderIds.has(String(r.rider_id)) + ); + if (!dirty.length) { + OpenToast('No edits to reconcile', 'info', 2500); + return; + } + reconcileMutation.mutate({ + riders: dirty.map((r) => ({ + rider_id: r.rider_id, + orders: r.orders + })) + }); + }; + + const openChangeRider = (oldRider, order) => { + const oldId = + oldRider?.rider_id ?? oldRider?.id ?? order?.rider_id ?? order?.userid ?? null; + setSelectedOldRiderId(oldId); + setSelectedOrder(order); + setSelectedNewRider(null); + setChangeDialogOpen(true); + }; + + const confirmChangeRider = () => { + if (!selectedNewRider || !selectedOrder) return; + // Backend expects an int — coerce at the boundary so a string from the + // riders API doesn't propagate into the Assign Orders payload. + const newRiderId = Number(selectedNewRider.userid); + const newRiderName = + selectedNewRider.label || + `${selectedNewRider.firstname || ''} ${selectedNewRider.lastname || ''}`.trim() || + `Rider ${newRiderId}`; + + setDispatchPreviewData((prev) => + moveOrderInPreviewData(prev, { + orderId: selectedOrder.orderid, + oldRiderId: selectedOldRiderId, + newRiderId, + newRiderName + }) + ); + // Both riders' step sequences are now potentially stale: the old rider + // lost a stop, the new rider gained one. Mark both as dirty so the next + // Reconcile sends exactly these two. + setDirtyRiderIds((prev) => { + const next = new Set(prev); + if (selectedOldRiderId != null) next.add(String(selectedOldRiderId)); + if (newRiderId != null && Number.isFinite(newRiderId)) next.add(String(newRiderId)); + return next; + }); + setHasReconciled(false); + setChangeDialogOpen(false); + OpenToast('Rider changed — click Reconcile to verify steps', 'info', 2500); + }; + + return ( + + theme.zIndex.modal + 1 }} + open={isLoading} + > + + + + + + + + navigate('/nearle/orders')} + sx={{ bgcolor: 'action.hover', '&:hover': { bgcolor: 'action.selected' } }} + > + + + + + Assign Orders + + + + option.type} + sx={{ minWidth: 250, maxWidth: 600, flex: 1 }} + renderInput={(params) => } + onChange={(e, val, reason) => { + if (reason === 'clear') handleCreateDelivery(null); + else handleCreateDelivery(val.value); + }} + /> + + + + + + + + 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} + + + ); + })} + + + ); + })} + + + + + + )} +
    + )} +
    + + + + + + + + + setChangeDialogOpen(false)} maxWidth="xs" fullWidth> + Change Rider + + + Move order #{selectedOrder?.orderid} (step {selectedOrder?.step ?? '—'}) to: + + + o?.label || `${o?.firstname || ''} ${o?.lastname || ''}`.trim() || '' + } + value={selectedNewRider} + onChange={(e, val) => setSelectedNewRider(val)} + renderInput={(params) => } + /> + + + + + + + + ); +}; + +// 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 ( - - ); -} -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'); - }} - /> - - - - - { - setSearch(e.target.value); - if (e.target.value == '') { - fetchdeliverylist(); - } - }} - sx={{ - '& .MuiOutlinedInput-input': { - p: '1.5px 0px 1px' - }, - width: '100%' - }} - // style={{ margin: "15px 20px" }} - startAdornment={ - - - - } - endAdornment={ - { - fetchdeliverylist(); - }} - > - - - } - /> - - {/* - 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 - - - - - - - - - - {/* ====================================== ||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={ + + } + /> + + + + + {/* ====================================== ||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" + /> + + + )} +
    - {/* - - */} -
    - +
    +
    {/* ================================================= || Drop || ================================================= */} - - - - - - Drop Details - - - {/* Customer */} - {/* + } + title="Drop Details" + subtitle="Where the order is delivered" + action={ + + } + /> + + + + + + Drop Details + + + {/* Customer */} + {/* { if (val) { @@ -1688,32 +1912,32 @@ const Createorder1 = () => { size="small" /> Business */} - + 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" + /> + + + )} +
    -
    - {/* */} -
    - +
    + + {/* ================================================= || 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 + + )} - - + + @@ -2371,176 +2654,243 @@ const Createorder1 = () => { {/* ============================================= || saved address Dialog || ============================================= */} { - setIsCustomerOpen(false); - }} + onClose={() => setIsCustomerOpen(false)} fullWidth - sx={{ minWidth: 'lg' }} + maxWidth="sm" + PaperProps={{ sx: { borderRadius: 3 } }} > - - - {`Select Location (${pickordrop === 1 ? 'Pickup' : 'Drop'})`} - - - setSearchCustList(e.target.value)} - // onChange={handleSearch} - sx={{ - '& .MuiOutlinedInput-input': { - p: '10.5px 0px 12px' - }, - bgcolor: 'white' - }} - startAdornment={ - - - - } - endAdornment={ - { - setSearchCustList(''); - }} - > - - - } - autoComplete="off" - /> - - + + + + + + + + Saved Locations + + + Pick a saved customer for {pickordrop === 1 ? 'Pickup' : 'Drop'} + + + + + setSearchCustList(e.target.value)} + sx={{ + flex: 1, + '& .MuiOutlinedInput-input': { p: '6px 4px', fontSize: 13, fontWeight: 600 }, + '& fieldset': { border: 'none' } + }} + endAdornment={ + searchCustList && ( + setSearchCustList('')} sx={{ p: 0.25, color: BRAND }}> + + + ) + } + autoComplete="off" + /> + - - - {customerlist.length == 0 ? ( - - + + {customerlist.length === 0 ? ( + + + + + + No saved locations + + + {searchCustList ? 'Try a different keyword.' : 'Save a customer from the order form to reuse it later.'} + ) : ( - - {customerlist && - customerlist.map((address, index) => ( - - ))} + + + ); + })} )} - - + - {/* */} - + - { - setOpen4(false); - }} - > - - - - Error - - - - - Service not available at this location - - - setOpen4(false)} PaperProps={{ sx: { borderRadius: 3 } }}> + { - setOpen4(false); + p: 2.5, + background: `linear-gradient(135deg, ${tint('#ef4444')} 0%, ${tint('#f59e0b')} 100%)`, + borderBottom: `1px solid ${DT.borderSubtle}` }} > - Close - + + + + + + + Out of Service Area + + + This drop point is outside the supported radius + + + + + + + Service is not available at this location. Try a different drop point within the coverage zone. + + + + + 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)} + - - - - + @@ -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') && - - } */} + + {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 || ============================================== */} - setCancelOpen(false)} maxWidth="xs"> - - - - + {/* ============================================= || Cancel Order Dialog || ============================================= */} + setCancelOpen(false)} maxWidth="xs" PaperProps={{ sx: { borderRadius: 3 } }}> + + + + - - - Are you sure you want to cancel this order? - - - + + Cancel Order + + + + + + + Are you sure you want to cancel this order? This action cannot be undone. + + {' '} + @@ -840,7 +1223,8 @@ const Orders = () => { - {/* ============================================== || Date filter || ============================================== */} + + {/* ============================================= || Date Filter || ============================================= */} setDateOpen(false)} @@ -850,7 +1234,7 @@ const Orders = () => { setDatestatus(label); }} /> - + ); }; diff --git a/src/pages/nearle/reports/orderSummary.js b/src/pages/nearle/reports/orderSummary.js index 1239f9d..b0d070f 100644 --- a/src/pages/nearle/reports/orderSummary.js +++ b/src/pages/nearle/reports/orderSummary.js @@ -1,16 +1,11 @@ -import { React, useState, useEffect, useRef, Fragment } from 'react'; +import React, { useState, useEffect, useRef, Fragment } from 'react'; import axios from 'axios'; import { useQuery } from '@tanstack/react-query'; -import { Empty } from 'antd'; -import TaskAltIcon from '@mui/icons-material/TaskAlt'; -import MyLocationIcon from '@mui/icons-material/MyLocation'; -import HighlightOffIcon from '@mui/icons-material/HighlightOff'; import { enqueueSnackbar } from 'notistack'; -import { CalendarMonth } from '@mui/icons-material'; -// material-ui import { + Avatar, Box, Table, TableBody, @@ -19,60 +14,172 @@ import { TableHead, TableRow, Dialog, - DialogTitle, - Typography, DialogContent, + Typography, Stack, Button, IconButton, Tooltip, - Chip, Autocomplete, TextField, - FormControl, - OutlinedInput, - InputAdornment, Collapse, - Badge + Skeleton, + Paper, + Grid, + InputBase } from '@mui/material'; -import ClearIcon from '@mui/icons-material/Clear'; -import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; -import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; -import { SearchOutlined } from '@ant-design/icons'; +import { + MdLocalShipping, + MdHourglassEmpty, + MdCheckCircle, + MdCancel, + MdMyLocation, + MdPlace, + MdSearch, + MdClear, + MdCalendarMonth, + MdReceiptLong, + MdStraighten, + MdCurrencyRupee, + MdInventory2, + MdKeyboardArrowDown, + MdKeyboardArrowUp, + MdTaskAlt, + MdHighlightOff, + MdInsights, + MdLocalOffer +} 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 { FilterList } from '@mui/icons-material'; +import { addDays, addMonths, addWeeks, endOfMonth, endOfWeek, startOfMonth, startOfWeek } from 'date-fns'; -// project imports - -import MainCard from 'components/MainCard'; import Loader from 'components/Loader'; import { useTheme } from '@mui/material/styles'; -import TitleCard from '../titleCard'; import { getreportlocationsummary, gettenantlocations } from '../api/api'; import CircularLoader from 'components/nearle_components/CircularLoader'; -import { getValueColor } from 'components/nearle_components/getValueColor'; + +// ============================================================================ +// 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 palette per metric column. +const C_ORDERS = '#0ea5e9'; +const C_DELIVERIES = BRAND; +const C_PENDING = '#f59e0b'; +const C_COMPLETED = '#10b981'; +const C_CANCELLED = '#ef4444'; +const C_ACCEPTED = '#6366f1'; +const C_PICKED = '#8b5cf6'; +const C_ARRIVED = '#06b6d4'; +const C_SKIPPED = '#f97316'; +const C_KMS = '#f59e0b'; + +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 } + } +}); + +// Inline metric pill used in cells; faded slate when zero. +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} + + ); +}; function formatNumberToRupees(value) { return new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', minimumFractionDigits: 2 - }).format(value); + }).format(value || 0); } const opentoast = (message, variant, time) => { @@ -83,19 +190,16 @@ const opentoast = (message, variant, time) => { }); }; -// ==============================|| MUI TABLE - ENHANCED ||============================== // +// ==============================|| OrdersReport ||============================== // export default function OrdersReport() { - // const [rows, setRows] = useState([]); const theme = useTheme(); - const bgcolor0 = theme.palette.primary.lighter; - const bgcolor1 = '#ffcdd2'; - const bgcolor2 = '#f8bbd0'; const tenantid = localStorage.getItem('tenantid'); + const [startdate, setStartdate] = useState(dayjs().format('YYYY-MM-DD')); const [enddate, setEnddate] = useState(dayjs().format('YYYY-MM-DD')); const [open, setOpen] = useState(false); - const [openRow, setOpenRow] = useState(null); // Initially no row is open + const [openRow, setOpenRow] = useState(null); const [datestatus, setDatestatus] = useState('Today'); const [total, settotal] = useState(0); const [totalOrders, settotalOrders] = useState(0); @@ -111,42 +215,33 @@ export default function OrdersReport() { const [ridersdata, setRidersdata] = useState(null); const [selectedLocation, setSelectedLocation] = useState(null); const [locationId, setLocationId] = useState(0); - const [searchLocation, setSearchLocation] = useState(''); + const [locoName, setLocoName] = useState('All Locations'); + const [searchLocation] = useState(''); + // Debounce search. useEffect(() => { const handler = setTimeout(() => { setDebouncedSearch(searchword); }, 400); - return () => clearTimeout(handler); }, [searchword]); - useEffect(() => { - console.log('openRow', openRow); - }, [openRow]); - - /* ============================================= || handleKeyPress (ctrl+k)| ============================================= */ + // Ctrl/Cmd+K to focus the 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); }, []); - // ============================================= || gettenantlocations (branches) || ============================================= - + // ============================================= || gettenantlocations || ============================================= const { data: tenantLocations, isLoading: tenantLocationsIsLoading, @@ -157,30 +252,30 @@ export default function OrdersReport() { queryFn: gettenantlocations }); - // ==============================|| fetchOrdersSummary (orders summary)||============================== // + // ============================================= || getreportlocationsummary || ============================================= const { isLoading: isLoadingReports, - isError: isErrorReports, //true or false + isError: isErrorReports, data: rows, error: reportsError } = useQuery({ queryKey: [startdate, enddate, locationId, debouncedSearch], queryFn: getreportlocationsummary }); - // ==============================|| getriderlocationsummary by tenid (orders summary)||============================== // + + // ============================================= || getriderlocationsummary || ============================================= const getriderlocationsummary = async (id) => { try { const riderRes = await axios.get( `${process.env.REACT_APP_URL}/deliveries/getriderlocationsummary/?&tenantid=${tenantid}&locationid=${id}&fromdate=${startdate}&todate=${enddate}` ); - console.log('riderRes', riderRes.data.details); setRidersdata(riderRes.data.details); } catch (error) { console.log('riderRes', error); } }; - // ==============================|| calculate||============================== // + // ============================================= || calculate totals || ============================================= const calculate = () => { let calculatedTotal = 0; let ordersTotal = 0; @@ -201,7 +296,6 @@ export default function OrdersReport() { deliverycomplete += row.deliveriescompleted; deliverycancel += row.deliveriescancelled; }); - // Update the state after the calculation is done settotal(calculatedTotal); settotalOrders(ordersTotal); setTotalOrderPend(Orderpending); @@ -216,575 +310,753 @@ export default function OrdersReport() { }, [rows]); let errormessage = ''; - if (isErrorReports && reportsError?.message) { errormessage = `An error has occurred: (isErrorReports) ${reportsError.message}`; } else if (tenantLocationsIsError && tenantLocationsError?.message) { errormessage = `An error has occurred: (tenantLocationsIsError) ${tenantLocationsError.message}`; } - useEffect(() => { - if (errormessage) { - console.log('errormessage', errormessage); - opentoast(errormessage, 'warning', 2000); - } + if (errormessage) opentoast(errormessage, 'warning', 2000); }, [errormessage]); + // KPI tiles — derived from the calculated totals. + const kpiCards = [ + { key: 'total', label: 'Total Orders', color: BRAND, icon: MdLocalShipping, value: totalOrders }, + { key: 'pending', label: 'Pending', color: C_PENDING, icon: MdHourglassEmpty, value: totalOrderPend }, + { key: 'completed', label: 'Completed', color: C_COMPLETED, icon: MdCheckCircle, value: totalOrderComplete }, + { key: 'cancelled', label: 'Cancelled', color: C_CANCELLED, icon: MdCancel, value: totalOrderCancel }, + { key: 'charges', label: 'Total Charges', color: C_ACCEPTED, icon: MdLocalOffer, value: total, isMoney: true } + ]; + return ( <> {(isLoadingReports || tenantLocationsIsLoading) && } {(isLoadingReports || tenantLocationsIsLoading) && } - - {startdate && enddate && ( - - - - - - {dayjs(startdate).format('DD/MM/YYYY')} - {dayjs(enddate).format('DD/MM/YYYY')} - - } - variant="combined" - color="warning" - size="small" - deleteIcon={} - onDelete={() => { - setOpen(true); + + {/* ============================================= || Header || ============================================= */} + + + + + + + + + Orders Summary + + + setOpen(true)} - sx={{ cursor: 'pointer' }} /> + + Live · {locoName} · {datestatus} + - )} - {(!startdate || !enddate) && ( - <> - - - - - )} + - } - />{' '} - - {/* Search Input */} - - + 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 || ============================================= */} + + + {/* Search */} + + + + setSearchword(e.target.value)} autoComplete="off" - size="medium" sx={{ - bgcolor: 'white', - borderRadius: 2 - }} - startAdornment={ - - - - } - endAdornment={ - - setSearchword('')}> - - - - } - /> - - - {/* Location Input */} - {tenantLocations?.length === 1 ? ( - - - - ), - sx: { bgcolor: 'white' } + fontSize: 13, + fontWeight: 600, + color: DT.textPrimary, + '& input::placeholder': { color: DT.textMuted, opacity: 1 } }} /> - ) : ( - `${o.locationname} (${o.suburb})`} - onChange={(event, value, reason) => { - setSelectedLocation(value); - setLocationId(value ? value.locationid : 0); - }} - sx={{ - minWidth: 250, - maxWidth: 400, - flex: 1 - }} - renderInput={(params) => ( - - )} - /> - )} - - } + {searchword && ( + setSearchword('')} sx={{ p: 0.25, color: BRAND }}> + + + )} + + + + {/* Location filter */} + {tenantLocations?.length === 1 ? ( + + {tenantLocations[0]?.locationname} + + ) : ( + (o ? `${o.locationname} (${o.suburb || ''})` : '')} + PaperComponent={SoftPaper} + onChange={(event, value) => { + setSelectedLocation(value); + setLocationId(value ? value.locationid : 0); + setLocoName(value ? value.locationname : 'All Locations'); + }} + renderInput={(params) => ( + + + + + + ) + }} + /> + )} + sx={{ width: { xs: '100%', md: 320 } }} + /> + )} + + + + {/* ============================================= || 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 + + + + - - + 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 */} + + + + { - setSearchword(e.target.value); - }} + onChange={(e) => setSearchword(e.target.value)} autoComplete="off" - startAdornment={ - - - - } - endAdornment={ - - { - setSearchword(''); - }} - > - - - - } - /> - - - {/* - 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 + + + + - - + - {/* ========================================= || 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 + + + - + + } sx={{ display: { xs: 'none', sm: 'inline-flex' } }}> + {dayjs().format('DD MMM YYYY')} + + + - {/* 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 && ( - - mantis - + + + + + + + Couldn’t load rider logs + + + The map is unavailable right now. Try refreshing in a moment. + + + + + + + )} - + ); }; 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) } ]} /> - - + 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"