diff --git a/src/App.js b/src/App.js index ba4e4e8..19c8933 100644 --- a/src/App.js +++ b/src/App.js @@ -15,6 +15,28 @@ import useInactivityLogout from 'hooks/useInactivityLogout'; // auth-provider // import { JWTProvider as AuthProvider } from 'contexts/JWTContext'; +// Defined at module scope, not inside App(), so it keeps a stable component +// identity across App re-renders โ€” nesting it inside App() meant every App +// render created a brand-new AppContent function, which React treats as a +// different component type and remounts (dropping Routes/Snackbar state and +// re-running the FCM setup effect) on every re-render. +const AppContent = () => { + useInactivityLogout(); + + useEffect(() => { + generateToken(); + const unsubscribe = initFirebaseNotificationListener(); + return () => unsubscribe?.(); + }, []); + + return ( + <> + + + + ); +}; + // ==============================|| APP - THEME, ROUTER, LOCAL ||============================== // const App = () => { @@ -25,22 +47,6 @@ const App = () => { } }, [navigate]); - const AppContent = () => { - useInactivityLogout(); - - useEffect(() => { - generateToken(); - initFirebaseNotificationListener(); - }, []); - - return ( - <> - - - - ); - }; - return ( <> diff --git a/src/components/nearle_components/GlobalToast.js b/src/components/nearle_components/GlobalToast.js index 8ab14d9..f4de73f 100644 --- a/src/components/nearle_components/GlobalToast.js +++ b/src/components/nearle_components/GlobalToast.js @@ -1,7 +1,7 @@ import { enqueueSnackbar, closeSnackbar } from 'notistack'; import { setSnackbarId } from '../../store/reducers/toastSlice'; -import { store, dispatch } from 'store'; +import { dispatch } from 'store'; export const GlobalToast = (message, color = 'default', vertical = 'top') => { const id = enqueueSnackbar(message, { @@ -20,11 +20,3 @@ export const GlobalToast = (message, color = 'default', vertical = 'top') => { return id; }; - -// GLOBAL close function -export const closeGlobalToast = () => { - const id = store.getState().toastSlice.snackbarId; - if (id) { - closeSnackbar(id); - } -}; diff --git a/src/components/nearle_components/LocationAutocomplete.js b/src/components/nearle_components/LocationAutocomplete.js index e892d43..1c844b9 100644 --- a/src/components/nearle_components/LocationAutocomplete.js +++ b/src/components/nearle_components/LocationAutocomplete.js @@ -12,6 +12,7 @@ import { fetchAppLocations } from 'pages/api/api'; const LocationAutocomplete = forwardRef( ( { + locaName, setAppId, setLocoName, setPage, @@ -92,6 +93,13 @@ const LocationAutocomplete = forwardRef( l.locationname === locaName) ?? null : null} + isOptionEqualToValue={(option, value) => option?.applocationid === value?.applocationid} getOptionLabel={(option) => option?.locationname ?? ''} PaperComponent={paperComponent} sx={{ ...sx }} diff --git a/src/components/nearle_components/SearchBar.js b/src/components/nearle_components/SearchBar.js deleted file mode 100644 index 4cc2bb6..0000000 --- a/src/components/nearle_components/SearchBar.js +++ /dev/null @@ -1,77 +0,0 @@ -// DebouncedSearchBar.jsx -import React, { useEffect, useRef, useState } from 'react'; -import { FormControl, OutlinedInput, InputAdornment, IconButton, useTheme } from '@mui/material'; -import SearchOutlined from '@mui/icons-material/SearchOutlined'; -import ClearIcon from '@mui/icons-material/Clear'; - -const SearchBar = ({ value, onChange, sx, placeholder = 'Search (Ctrl + K)', delay = 300 }) => { - const theme = useTheme(); - const inputRef = useRef(null); - - // Local input state for immediate UI update - const [inputValue, setInputValue] = useState(value || ''); - - /* Sync external value when it changes */ - useEffect(() => { - setInputValue(value || ''); - }, [value]); - - /* ================================ - ๐Ÿ”ฅ Debounce Logic - ================================ */ - useEffect(() => { - const handler = setTimeout(() => { - if (inputValue !== value) { - onChange({ target: { value: inputValue } }); - } - }, delay); - - return () => clearTimeout(handler); - }, [inputValue]); - - /* CTRL + K / CMD + K & ESC behavior */ - useEffect(() => { - const handleKeyPress = (event) => { - if (event.key === 'k' && (event.metaKey || event.ctrlKey)) { - event.preventDefault(); - inputRef.current?.focus(); - } - - if (event.key === 'Escape' && document.activeElement === inputRef.current) { - inputRef.current.blur(); - } - }; - - document.addEventListener('keydown', handleKeyPress); - return () => document.removeEventListener('keydown', handleKeyPress); - }, []); - - return ( - - setInputValue(e.target.value)} // No direct onChange - autoComplete="off" - size="large" - startAdornment={ - - - - } - endAdornment={ - setInputValue('')}> - - - } - /> - - ); -}; - -export default SearchBar; diff --git a/src/components/nearle_components/StatusChip.js b/src/components/nearle_components/StatusChip.js deleted file mode 100644 index 6950687..0000000 --- a/src/components/nearle_components/StatusChip.js +++ /dev/null @@ -1,74 +0,0 @@ -import PropTypes from 'prop-types'; -import { Chip } from '@mui/material'; - -// ==============================|| STATUS CHIP (Doormile-style soft badge) ||============================== // -// One consistent soft-filled chip for every lifecycle status across orders, -// deliveries, riders, tenants, invoices. Colour encodes meaning; the chip stays -// quiet (soft bg + readable dark text) so tables don't turn into a rainbow. - -// Soft background + readable foreground per semantic tone. -const TONE = { - amber: { bg: '#FEF3C7', fg: '#92400E' }, - indigo: { bg: '#E0E7FF', fg: '#3730A3' }, - cyan: { bg: '#CFFAFE', fg: '#155E75' }, - violet: { bg: '#EDE9FE', fg: '#5B21B6' }, - teal: { bg: '#CCFBF1', fg: '#115E59' }, - emerald: { bg: '#D1FAE5', fg: '#065F46' }, - red: { bg: '#FEE2E2', fg: '#991B1B' }, - orange: { bg: '#FFEDD5', fg: '#9A3412' }, - sky: { bg: '#E0F2FE', fg: '#075985' }, - slate: { bg: '#F1F5F9', fg: '#475569' }, - brand: { bg: '#F1E6F7', fg: '#5B1F73' } -}; - -// Status keyword -> tone + display label. -const MAP = { - pending: { tone: 'amber', label: 'Pending' }, - created: { tone: 'sky', label: 'Created' }, - assigned: { tone: 'indigo', label: 'Assigned' }, - accepted: { tone: 'indigo', label: 'Accepted' }, - arrived: { tone: 'cyan', label: 'Arrived' }, - picked: { tone: 'violet', label: 'Picked' }, - 'picked-up':{ tone: 'violet', label: 'Picked Up' }, - started: { tone: 'cyan', label: 'Started' }, - active: { tone: 'teal', label: 'Active' }, - 'in-transit': { tone: 'teal', label: 'In Transit' }, - delivered: { tone: 'emerald', label: 'Delivered' }, - completed: { tone: 'emerald', label: 'Completed' }, - skipped: { tone: 'orange', label: 'Skipped' }, - failed: { tone: 'red', label: 'Failed' }, - cancelled: { tone: 'red', label: 'Cancelled' }, - // riders / tenants - online: { tone: 'emerald', label: 'Online' }, - offline: { tone: 'slate', label: 'Offline' }, - inactive: { tone: 'slate', label: 'Inactive' }, - idle: { tone: 'amber', label: 'Idle' }, - unknown: { tone: 'slate', label: 'Unknown' }, - // invoices - paid: { tone: 'emerald', label: 'Paid' }, - unpaid: { tone: 'amber', label: 'Unpaid' }, - open: { tone: 'sky', label: 'Open' }, - overdue: { tone: 'red', label: 'Overdue' }, - prepaid: { tone: 'emerald', label: 'Prepaid' }, - cod: { tone: 'amber', label: 'COD' } -}; - -export default function StatusChip({ status, label, size = 'small', sx }) { - const key = String(status || '').toLowerCase().trim().replace(/\s+/g, '-'); - const cfg = MAP[key] || { tone: 'slate', label: status || 'โ€”' }; - const tone = TONE[cfg.tone] || TONE.slate; - return ( - - ); -} - -StatusChip.propTypes = { - status: PropTypes.string, - label: PropTypes.node, - size: PropTypes.string, - sx: PropTypes.object -}; diff --git a/src/components/nearle_components/TitleCard.js b/src/components/nearle_components/TitleCard.js deleted file mode 100644 index cfcaf38..0000000 --- a/src/components/nearle_components/TitleCard.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { Stack, Typography, Box } from '@mui/material'; - -const TitleCard = ({ sx, title, children, starticon }) => { - return ( - - - - {starticon && starticon} - {title} - - {children} - - - ); -}; - -export default TitleCard; diff --git a/src/components/updateNetworkStatus.js b/src/components/updateNetworkStatus.js index b5f9e15..650ae01 100644 --- a/src/components/updateNetworkStatus.js +++ b/src/components/updateNetworkStatus.js @@ -32,7 +32,6 @@ const InternetStatus = () => { setIsOnline(online); if (online && wasOffline.current) { - console.log('โœ… Back online'); setShowBackOnline(true); wasOffline.current = false; // window.location.reload(); @@ -43,7 +42,6 @@ const InternetStatus = () => { } if (!online) { - console.log('โŒ No internet connection'); wasOffline.current = true; setShowBackOnline(false); } diff --git a/src/firebase_notification/notification.js b/src/firebase_notification/notification.js index b02582d..b96f399 100644 --- a/src/firebase_notification/notification.js +++ b/src/firebase_notification/notification.js @@ -44,7 +44,6 @@ export const generateToken = async () => { }); if (token) { - console.log('๐Ÿ“Œ FCM Token:', token); dispatch(setFcmToken(token)); } else { console.warn('โš ๏ธ No FCM token generated'); @@ -55,12 +54,11 @@ export const generateToken = async () => { }; // ===================== Foreground Notifications ===================== -export const initFirebaseNotificationListener = () => { - console.log('๐Ÿ”ฅ Firebase foreground listener initialized'); - +// Returns the unsubscribe function onMessage() gives back, so callers can +// clean it up on unmount instead of leaking a duplicate listener on every +// remount. +export const initFirebaseNotificationListener = () => onMessage(messaging, async (payload) => { - console.log('๐Ÿ“ฅ Foreground message:', payload); - const { notification = {}, data = {} } = payload; // โœ… UI Toast (your custom toast) @@ -85,4 +83,3 @@ export const initFirebaseNotificationListener = () => { } } }); -}; diff --git a/src/layout/MainLayout/AppTopNav.js b/src/layout/MainLayout/AppTopNav.js index eaa51c1..914dd0c 100644 --- a/src/layout/MainLayout/AppTopNav.js +++ b/src/layout/MainLayout/AppTopNav.js @@ -24,7 +24,8 @@ import { LogoutOutlined, GiftOutlined, MessageOutlined, - SettingOutlined + SettingOutlined, + UserAddOutlined } from '@ant-design/icons'; import avatar1 from 'assets/images/users/avatar-1.png'; @@ -117,7 +118,8 @@ const AppTopNav = () => { style={{ backgroundColor: DT.surface, borderBottom: `1px solid ${DT.borderSubtle}`, - boxShadow: DT.shadowMd + boxShadow: DT.shadowMd, + zIndex: 1200 }} heading={} headingHref="/doormile/dispatch" />} endContent={ @@ -127,7 +129,8 @@ const AppTopNav = () => { button={{ label: 'Quick create', icon: , isIconOnly: true, variant: 'ghost' }} items={[ { label: 'Create Order', icon: , onClick: () => navigate('/doormile/orders/create') }, - { label: 'Create Multiple Order', icon: , onClick: () => navigate('/doormile/orders/createorders') } + { label: 'Create Multiple Order', icon: , onClick: () => navigate('/doormile/orders/createorders') }, + { label: 'Create Customer', icon: , onClick: () => navigate('/doormile/customer/create') } ]} /> @@ -156,6 +159,13 @@ const AppTopNav = () => { .astryx-app-shell { --appshell-header-height: ${headerHeight}px !important; } + /* Force TopNav wrapper to have a solid background and high z-index to fix the astryx AppShell bug */ + .astryx-app-shell-header { + z-index: 1200 !important; + background-color: #ffffff !important; /* Equivalent to DT.surface */ + border-bottom: 1px solid #e2e8f0 !important; + box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06) !important; + } `} )} diff --git a/src/layout/MainLayout/index.js b/src/layout/MainLayout/index.js index 42249ad..8d07cd6 100644 --- a/src/layout/MainLayout/index.js +++ b/src/layout/MainLayout/index.js @@ -33,24 +33,22 @@ const MainLayout = () => { } sideNav={} mobileNav={{ breakpoint: 'lg' }} > -
+
- ); diff --git a/src/menu-items/nearle.js b/src/menu-items/nearle.js index 2d82fcc..5d58751 100644 --- a/src/menu-items/nearle.js +++ b/src/menu-items/nearle.js @@ -31,7 +31,8 @@ import { IdcardOutlined, TruckOutlined, WarningOutlined, - RadarChartOutlined + RadarChartOutlined, + RiseOutlined } from '@ant-design/icons'; // icons @@ -64,7 +65,8 @@ const icons = { IdcardOutlined, TruckOutlined, WarningOutlined, - RadarChartOutlined + RadarChartOutlined, + RiseOutlined }; // ==============================|| MENU ITEMS - SUPPORT ||============================== // @@ -200,6 +202,13 @@ const nearle = { type: 'item', url: '/doormile/reports/riderssummary', icon: icons.UserOutlined + }, + { + id: 'profitability', + title: , + type: 'item', + url: '/doormile/reports/profitability', + icon: icons.RiseOutlined } ] } diff --git a/src/pages/api/api.js b/src/pages/api/api.js index 07a7fb1..300e4c0 100644 --- a/src/pages/api/api.js +++ b/src/pages/api/api.js @@ -1,5 +1,6 @@ import axios from 'axios'; import { OpenToast } from 'components/third-party/OpenToast'; +import logger from 'utils/logger'; import { getMilers, getMiler, @@ -14,13 +15,10 @@ import { getBooking, assignMilerToBooking, cancelBooking, - bulkCancelBookings, updateConsignmentStatus, getAdminPricing, - getPartners, getReports, getLocationsSummary, - getDashboard, getProfile } from 'pages/api/doormileApi'; @@ -88,8 +86,10 @@ export const getRiderPeriodicLogs = async (userid) => { const logs = await getMilerLogs(userid, undefined, undefined, 1); const latest = Array.isArray(logs) ? logs[0] : logs; if (latest) return latest; - } catch (err) { - console.log('getRiderPeriodicLogs', err.message); + } catch { + // Falls through to the getMiler() fallback below โ€” this is polled every + // 15s by the live map, so a toast here would spam the operator on every + // failed tick. Only surface an error if the fallback also fails. } try { return (await getMiler(userid)) || null; @@ -122,25 +122,35 @@ export const fetchAppLocations = async () => { // the doc only promises "counts + today's numbers", not an exact schema. export const fetchPercentageData = async ({ queryKey }) => { const [, , startdate, enddate] = queryKey; - const details = (await getReports(startdate, enddate)) || {}; - const total = details.total || 0; - return { - created: String(details.created || 0), - uncoveredOrders: String(details.pending || 0), - coveredOrders: String(details.delivered || 0), - cancelled: String(details.cancelled || 0), - percentage1: String(Math.round(((details.created || 0) / total) * 100) || 0), - percentage2: String(Math.round(((details.pending || 0) / total) * 100) || 0), - percentage3: String(Math.round(((details.delivered || 0) / total) * 100) || 0), - percentage4: String(Math.round(((details.cancelled || 0) / total) * 100) || 0) - }; + try { + const details = (await getReports(startdate, enddate)) || {}; + const total = details.total || 0; + return { + created: String(details.created || 0), + uncoveredOrders: String(details.pending || 0), + coveredOrders: String(details.delivered || 0), + cancelled: String(details.cancelled || 0), + percentage1: String(Math.round(((details.created || 0) / total) * 100) || 0), + percentage2: String(Math.round(((details.pending || 0) / total) * 100) || 0), + percentage3: String(Math.round(((details.delivered || 0) / total) * 100) || 0), + percentage4: String(Math.round(((details.cancelled || 0) / total) * 100) || 0) + }; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load order percentages', 'error', 2000); + return { created: '0', uncoveredOrders: '0', coveredOrders: '0', cancelled: '0', percentage1: '0', percentage2: '0', percentage3: '0', percentage4: '0' }; + } }; // ===================================================== || getTenants || ===================================================== export const getTenants = async () => { - const tenants = await getAdminTenants(); - return (tenants || []).map((val) => ({ ...val, label: val.tenantname })); + try { + const tenants = await getAdminTenants(); + return (tenants || []).map((val) => ({ ...val, label: val.tenantname })); + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load tenants', 'error', 2000); + return []; + } }; // ============================================= || gettenantlocations (branches) || ============================================= @@ -148,31 +158,14 @@ export const gettenantlocations = async (tenantId) => { try { return (await getTenantLocations(tenantId)) || []; } catch (err) { - console.log('gettenantlocations', err); + // Log only the message โ€” the raw error object carries the request + // config, including the Authorization: Bearer header + // doormileAxios attaches, which shouldn't hit the console. + OpenToast(err.response?.data?.message || err.message || 'Failed to load tenant locations', 'error', 2000); return []; } }; -// ==============================|| fetchorderscount (orders) ||============================== // - -export const fetchorderscount = async ({ queryKey }) => { - const [, , startdate, enddate] = queryKey; - return (await getReports(startdate, enddate)) || {}; -}; - -// ==============================|| fetchOrders (orders) ||============================== // -// Maps to /admin/bookings. The old endpoint's applocationid/tenantid/locationid -// filters and free-text `status` are not documented on this resource โ€” pageno/ -// pagesize are (see "Conventions" in express-console-api.md). -export const fetchOrders = async ({ pageParam = 1, queryKey }) => { - const [, , , , , , rowsPerPage] = queryKey; - const rows = (await getBookings(pageParam, rowsPerPage)) || []; - return { - rows, - nextPage: rows.length === Number(rowsPerPage) ? pageParam + 1 : undefined - }; -}; - // ==============================|| fetchPaymentType (orders) ||============================== // // No payment-mode-types endpoint in the new API. export const fetchPaymentType = async () => []; @@ -187,8 +180,14 @@ export const fetchRidersList = async () => { label: `${val.displayname || val.authname || ''} | ${val.contactno || ''}` })); } catch (err) { - OpenToast(err.message, 'error', 2000); - throw err; + // Was `throw`ing after already toasting here โ€” deliveries.js also wires + // its own onError toast on this same query, so a real failure showed + // the operator the same error twice. Matches every sibling function's + // convention now: toast once, return a safe default, never throw (the + // other 4 call sites had no onError of their own and got zero feedback + // on failure before this fix). + OpenToast(err.response?.data?.message || err.message || 'Failed to load riders', 'error', 2000); + return []; } }; @@ -217,21 +216,128 @@ export const fetchBatchEfficiency = async ({ batch, tenantId }) => { return response.data; }; // ==============================|| finalCreatedeliveries (orders) ||============================== // -// Final delivery commit โ€” also part of the separate solver/optimiser pipeline, -// left untouched (still posts to jupiter.nearle.app directly, not via -// REACT_APP_URL, so this migration doesn't need to change it). +// Final delivery commit. Used to POST the whole batch to jupiter's +// /deliveries/createdeliveries in one shot โ€” retired for two confirmed +// reasons: (1) it 500'd because jupiter's Deliveries Go struct types +// pickuplat/pickuplon/droplat/droplon as string, while GET /admin/bookings +// (the new Doormile API) feeds orders.js's solver payload with those as +// numbers; (2) even once that's patched, jupiter writes into jupiter's own +// now-orphaned database โ€” neither the Orders "pending" list nor the +// Deliveries "dispatched" filter read it, since both come from Doormile's +// GET /admin/bookings. jupiter2doormile.md ยง4 (the "assign a rider" row, +// mapped from jupiter's old PUT /deliveries/updatedelivery) and +// express-console-api.md's exercised-routes list both confirm +// POST /admin/bookings/:id/assign-miler as the correct, live-proven +// replacement for this exact action โ€” not POST /admin/expressbooking +// (that one creates a brand-new booking from scratch; wrong here, since +// every order already exists as a Doormile booking pulled from +// GET /admin/bookings). 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; + const deliveries = deliveryData.deliveries || []; + logger.debug(`finalCreatedeliveries: ${deliveries.length} order(s) to assign`); + if (!deliveries.length) return { success: true, assigned: 0, failed: 0 }; + + // Diagnostic only (no PII beyond a rider's display name, which every + // operator already sees in the UI). Shows exactly which candidate field + // names are actually present, so a wrong guess in the fallback chain + // below is visible instead of silently mismatching. + deliveries.forEach((d, i) => { + logger.debug(` order[${i}] id fields:`, { + bookingid: d.bookingid, + orderheaderid: d.orderheaderid, + deliveryid: d.deliveryid, + orderid: d.orderid, + rider_id: d.rider_id, + userid: d.userid, + rider_name: d.rider_name ?? d.rider + }); + }); + + // assign-miler needs a milerprofileid. userid/milerprofileid matching was + // tried first (the solver's rider pool is fed full miler objects โ€” see + // Preview.js's handleCreateDelivery -> `riders: autoRiders`, raw + // GET /admin/milers data carrying both fields per rider) but confirmed + // live that NEITHER matches: the solver's rider_id/userid (e.g. "883") + // isn't in GET /admin/milers under either field. This matches + // Dispatch.js's own Analysis-panel comment โ€” "the workolik solver doesn't + // have our auth/users table" โ€” so its numeric rider ids are its own + // internal numbering, disconnected from Doormile's id space entirely. + // Falls back to matching the rider's NAME (also echoed by the solver, + // see flattenRiders' rider_name) against each miler's displayname/ + // authname โ€” the only other correlatable field. Logs the full roster + // alongside so a further mismatch is fully diagnosable from one run. + let milers = []; + try { + milers = (await getMilers()) || []; + } catch (err) { + logger.error('finalCreatedeliveries: GET /admin/milers failed', err.response?.status, err.response?.data || err.message); + } + logger.debug( + `finalCreatedeliveries: ${milers.length} miler(s) available for rider resolution`, + milers.map((m) => ({ userid: m.userid, milerprofileid: m.milerprofileid, name: m.displayname || m.authname })) + ); + const milerByUserId = new Map(milers.map((m) => [String(m.userid), m])); + const milerByProfileId = new Map(milers.map((m) => [String(m.milerprofileid), m])); + const normName = (s) => String(s || '').trim().toLowerCase(); + const milerByName = new Map(); + milers.forEach((m) => { + [m.displayname, m.authname].forEach((n) => { + const key = normName(n); + if (key && !milerByName.has(key)) milerByName.set(key, m); + }); + }); + + const results = await Promise.allSettled( + deliveries.map(async (d) => { + // Booking id: the solver is a separate, unverified service (see this + // area's CLAUDE.md) โ€” falls back through every plausible field name + // an order might carry it under, `bookingid` first since that's what + // orders.js actually sends in and most solvers pass unknown fields + // through untouched. + const bookingId = d.bookingid ?? d.orderheaderid ?? d.deliveryid ?? d.orderid; + const riderUserId = d.rider_id ?? d.userid; + const riderName = d.rider_name ?? d.rider; + const matchedVia = milerByUserId.has(String(riderUserId)) + ? 'userid' + : milerByProfileId.has(String(riderUserId)) + ? 'milerprofileid' + : milerByName.has(normName(riderName)) + ? 'name' + : null; + const rider = + milerByUserId.get(String(riderUserId)) ?? milerByProfileId.get(String(riderUserId)) ?? milerByName.get(normName(riderName)); + if (bookingId == null || !rider?.milerprofileid) { + const reason = + bookingId == null + ? 'no booking id resolved on the order object' + : `no miler found for rider id ${riderUserId} / name "${riderName}" (checked userid, milerprofileid, name)`; + logger.error(`finalCreatedeliveries: skipping order โ€” ${reason}`); + throw new Error(`order ${d.orderid ?? bookingId ?? '?'}: ${reason}`); + } + logger.debug(`finalCreatedeliveries: order booking ${bookingId} -> rider ${riderUserId} ("${riderName}") matched via ${matchedVia}`); + try { + return await assignMilerToBooking(bookingId, { milerid: rider.milerprofileid }); + } catch (err) { + logger.error( + `finalCreatedeliveries: assign-miler failed for booking ${bookingId}`, + err.response?.status, + err.response?.data || err.message + ); + throw err; + } + }) + ); + + const failed = results.filter((r) => r.status === 'rejected'); + if (failed.length === deliveries.length) { + // Total failure โ€” throw so the caller's mutation onError fires instead + // of a false "success" toast + navigate-away with nothing assigned. + throw new Error(`Couldn't assign any of the ${deliveries.length} order(s)`); + } + if (failed.length) { + OpenToast(`${failed.length} of ${deliveries.length} order(s) couldn't be assigned โ€” check Orders/Deliveries`, 'warning', 4000); + } + return { success: true, assigned: deliveries.length - failed.length, failed: failed.length }; }; // ==============================|| createAutomationDeliveries (orders) Auto rider Assign ||============================== // // Also part of the optimiser pipeline (routes.workolik.com / routemate.workolik.com) โ€” untouched. @@ -263,17 +369,6 @@ export const notifyRider = async ( return notifyMiler(milerProfileId, title, message); }; -// ==============================|| cancelOrder (orders) ||============================== // - -export const cancelOrder = async (bookingId) => cancelBooking(bookingId); - -// ==============================|| cancelMultipleOrder (orders) ||============================== // - -export const cancelMultipleOrder = async (orderlist) => { - const bookingIds = (orderlist || []).map((e) => e.bookingid ?? e.id); - return bulkCancelBookings(bookingIds); -}; - // ==============================|| fetchDeliveries (deliveries) ||============================== // // GET /admin/consignments has zero documented response schema (see // express-console-api.md โ€” it's listed with no example body at all, unlike @@ -297,8 +392,19 @@ export const cancelMultipleOrder = async (orderlist) => { // approach as fetchAllRiders' `{details}` wrapper. export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => { const [, , , , , , rowsPerPage] = queryKey; - const [bookings, customers, milers, tenants] = await Promise.all([ - getBookings(pageParam, rowsPerPage), + // Unlike the 3 joins below (customers/milers/tenants โ€” each individually + // guarded so a failed join just degrades a display field, not the whole + // page), a failed bookings call is the one thing this function can't + // recover from โ€” the previous version let it reject the whole Promise.all + // uncaught, with no toast anywhere across the 4 pages that call this. + let bookings; + try { + bookings = await getBookings(pageParam, rowsPerPage); + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load deliveries', 'error', 2000); + return { rows: [], nextPage: undefined }; + } + const [customers, milers, tenants] = await Promise.all([ getAdminCustomers().catch(() => []), getMilers().catch(() => []), getAdminTenants().catch(() => []) @@ -319,6 +425,7 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => { deliveryid: b.bookingid, orderid: b.bookingno || `#${b.bookingid}`, consignmentid: b.consignmentid, + tenantid: b.tenantid, tenantname: tenant?.tenantname || '', tenantsuburb: '', applocation: '', @@ -383,38 +490,24 @@ export const fetchDeliveries = async ({ pageParam = 1, queryKey }) => { }; }; -// Shared by fetchPercentageAPI/fetchCountAPI so the chip counts are derived -// from the exact same source (and status mapping) as fetchDeliveries' rows โ€” -// counting against the separately-guessed /admin/dashboard shape produced -// numbers that didn't match what the table actually showed. +// Backs fetchCountAPI so the chip counts are derived from the exact same +// source (and status mapping) as fetchDeliveries' rows โ€” counting against +// the separately-guessed /admin/dashboard shape produced numbers that didn't +// match what the table actually showed. const getDeliveryStatusCounts = async () => { - const bookings = (await getBookings(1, 1000)) || []; - const dispatched = bookings.filter((b) => b.assignedmileruserid || b.consignmentid); - const counts = { total: dispatched.length }; - dispatched.forEach((b) => { - const status = mapBookingStatusToDeliveryStatus(b.status); - counts[status] = (counts[status] || 0) + 1; - }); - return counts; -}; - -// ==============================|| fetchPercentageAPI (deliveries) ||============================== // - -export const fetchPercentageAPI = async () => { - const data = await getDeliveryStatusCounts(); - return { - coveredOrders: String(data.delivered || 0), - cancelledOrders: String(data.cancelled || 0), - uncoveredOrders: String(data.pending || 0), - assignedOrders: String(data.accepted || 0), - createdOrders: String(data.total || 0), - closedOrders: String(data.delivered || 0), - pickedOrders: String(data.picked || 0), - percentage1: String(Math.round(((data.pending || 0) / (data.total || 1)) * 100) || 0), - percentage2: String(Math.round(((data.accepted || 0) / (data.total || 1)) * 100) || 0), - percentage3: String(Math.round(((data.picked || 0) / (data.total || 1)) * 100) || 0), - percentage4: String(Math.round(((data.delivered || 0) / (data.total || 1)) * 100) || 0) - }; + try { + const bookings = (await getBookings(1, 1000)) || []; + const dispatched = bookings.filter((b) => b.assignedmileruserid || b.consignmentid); + const counts = { total: dispatched.length }; + dispatched.forEach((b) => { + const status = mapBookingStatusToDeliveryStatus(b.status); + counts[status] = (counts[status] || 0) + 1; + }); + return counts; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load delivery counts', 'error', 2000); + return {}; + } }; // ==============================|| fetchCountAPI (deliveries) ||============================== // @@ -473,21 +566,6 @@ export const getalltenants = async () => { return null; } }; -// ==============================|| gettenantsummary (tenants) ||============================== // -// No tenant-tab-counts endpoint in the new API. -export const gettenantsummary = async () => null; - -// ==============================|| getpricinglist (tenants) ||============================== // - -export const getpricinglist = async () => { - try { - return (await getAdminPricing()) || []; - } catch (err) { - const message = err.response?.data?.message || err.message || 'Something went wrong'; - OpenToast(message); - return null; - } -}; // ==============================|| getallpricing (clientPricing) ||============================== // // This is the client-facing "Pricing" page (tenant pricing rules, GET // /admin/pricing) โ€” not GET /admin/doormile-pricing, which is Doormile's own @@ -510,7 +588,7 @@ export const fetchAllRiders = async () => { const milers = await getMilers(); return { details: milers || [], nextPage: undefined }; } catch (err) { - console.log('fetchAllRiders err', err.message); + OpenToast(err.response?.data?.message || err.message || 'Failed to load riders', 'error', 2000); return { details: [], nextPage: undefined }; } }; @@ -519,36 +597,31 @@ export const getallridersummary = async () => { try { const milers = (await getMilers()) || []; // A miler has no `status` field โ€” availabilitystatus is the real one - // (confirmed live: Available/Assigned/On_Pickup/Offline). "Active" here - // means not Offline, matching riders.js's own tab filter. - const active = milers.filter((m) => String(m.availabilitystatus || '').toLowerCase() !== 'offline').length; + // (confirmed live: Available/Assigned/On_Pickup/Offline/Blocked). + // "Active" here means not Offline and not Blocked, matching riders.js's + // own tab filter. + const active = milers.filter((m) => !['offline', 'blocked'].includes(String(m.availabilitystatus || '').toLowerCase())).length; return { total: milers.length, active, inactive: milers.length - active }; } catch (err) { - console.log('getallridersummary err', err.message); + OpenToast(err.response?.data?.message || err.message || 'Failed to load rider summary', 'error', 2000); return { total: 0, active: 0, inactive: 0 }; } }; -// ==============================|| fetchRiders (riders), active riders ||============================== // - -export const fetchRiders = async () => { - try { - const milers = await getMilers(); - return { details: milers || [], nextPage: undefined }; - } catch (err) { - console.log('fetchRiders err', err.message); - return []; - } -}; - -// ==============================|| getriderstatus (riders)||============================== // -// No check-in-history endpoint in the new API. -export const getriderstatus = async () => null; - // ==============================|| getreportsummary (orders summary)||============================== // +// appId (queryKey[0]) was previously discarded here โ€” the LocationAutocomplete +// zone picker on the Orders Summary page changed the header text but never +// the actual request. GET /admin/reports does accept a hubid param (see +// getReports in doormileApi.js) โ€” hubs are this API's zone-equivalent (see +// LocationAutocomplete's own comment) โ€” so pass appId through as that. export const getreportsummary = async ({ queryKey }) => { - const [, tenantid, locationid, startdate, enddate] = queryKey; - return (await getReports(startdate, enddate, tenantid, locationid)) || {}; + const [appId, tenantid, locationid, startdate, enddate] = queryKey; + try { + return (await getReports(startdate, enddate, tenantid, locationid, appId || undefined)) || {}; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load the report summary', 'error', 2000); + return {}; + } }; // ==============================|| getreportlocationsummary (orders summary)||============================== // @@ -556,24 +629,14 @@ export const getreportsummary = async ({ queryKey }) => { // jupiter's getlocationsummary โ€” GET /admin/locations/summary. export const getreportlocationsummary = async ({ queryKey }) => { const [, tenantid, locationid, startdate, enddate] = queryKey; - return (await getLocationsSummary(tenantid, locationid, startdate, enddate)) || []; + try { + return (await getLocationsSummary(tenantid, locationid, startdate, enddate)) || []; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load the location summary', 'error', 2000); + return []; + } }; -// ==============================|| fetchorderdetails (orders detail)||============================== // -export const fetchorderdetails = async ({ queryKey }) => { - const [, , , page, rowsPerPage] = queryKey; - const rows = (await getBookings(page, rowsPerPage)) || []; - return rows.map((item, index) => ({ ...item, sno: index + 1 })); -}; - -// ==============================|| getriderbydelivery (orders detail)||============================== // -// No per-rider-delivery-trace endpoint in the new API. -export const getriderbydelivery = async () => []; - -// ==============================|| fetchCount (orders detail)||============================== // - -export const fetchCount = async () => (await getDashboard()) || {}; - // ==============================|| fetchRidersSummary (riders summary)||============================== // // No per-rider performance summary endpoint in the new API. // Confirmed against jupiter2doormile.md (Status: Done) as the replacement @@ -582,14 +645,13 @@ export const fetchCount = async () => (await getDashboard()) || {}; // as-is and lets the page's own rendering degrade gracefully on unknown keys. export const fetchRidersSummary = async ({ queryKey }) => { const [, appId, startdate, enddate] = queryKey; - const data = await getMilerSummary(appId || undefined, startdate, enddate); - return Array.isArray(data) ? data : data ? [data] : []; -}; - -// ==============================|| fetchLocations (orders summary))||============================== // -export const fetchLocations = async () => { - const partners = (await getPartners()) || []; - return [...partners, { partnername: 'All', partnerid: -1 }]; + try { + const data = await getMilerSummary(appId || undefined, startdate, enddate); + return Array.isArray(data) ? data : data ? [data] : []; + } catch (err) { + OpenToast(err.response?.data?.message || err.message || 'Failed to load the riders summary', 'error', 2000); + return []; + } }; // ==============================|| fetchRidersLogs (RiderLogs)||============================== // @@ -627,26 +689,21 @@ export const fetchRidersLogs = async ({ queryKey } = {}) => { latitude: r.currentlatitude, longitude: r.currentlongitude })); - } catch (err) { - console.log('fetchRidersLogs', err.message); + } catch { + // Polled every 1s by Dispatch.js's live map (RIDER_LOG_POLL_MS) โ€” a toast + // here would fire once a second on any outage. Fail silently; the map + // just stops updating rider pins until the next successful poll. return []; } }; -// ==============================|| getorders (Locations)||============================== // - -export const fetchOrders1 = async ({ pageParam = 1, queryKey }) => { - const [, , , , , , , rowsPerPage] = queryKey; - const rows = (await getBookings(pageParam, rowsPerPage)) || []; - return { details: rows, nextPage: rows.length === rowsPerPage ? pageParam + 1 : undefined }; -}; // ==============================|| getusers (viewProfile)||============================== // export const getusers = async () => { try { return (await getProfile()) || {}; } catch (err) { - console.log('getusers', err.message); + OpenToast(err.response?.data?.message || err.message || 'Failed to load profile', 'error', 2000); return {}; } }; @@ -656,7 +713,7 @@ export const getallriders = async () => { try { return (await getMilers()) || []; } catch (err) { - console.log('getallriders', err.message); + OpenToast(err.response?.data?.message || err.message || 'Failed to load riders', 'error', 2000); return []; } }; diff --git a/src/pages/api/doormileApi.js b/src/pages/api/doormileApi.js index 0aefc7a..430a894 100644 --- a/src/pages/api/doormileApi.js +++ b/src/pages/api/doormileApi.js @@ -4,7 +4,9 @@ import doormileAxios, { DOORMILE_TOKEN_KEY, DOORMILE_USER_KEY } from 'utils/door // Mirrors the shape of pages/api/api.js (named async exports, one per endpoint) but talks // to a different backend with a different envelope: { success, data } / { success, message }. // See express-console-api.md at the repo root for the full reference this was built from. -// Not wired into any page yet โ€” this is plumbing only. +// Consumed by pages/api/api.js and directly by several pages (createorder1.js, +// multipleOrders.js, riders.js, editRider.js, Tenants.js, competitiveIntel.js, +// hubs.js, vehicles.js, login.js). const buildQuery = (params = {}) => { const usp = new URLSearchParams(); @@ -516,9 +518,15 @@ export const updateExceptionStatus = async (id, status, resolution) => { // ==============================|| Competitive intel ||============================== // -export const getCompetitorBranches = async () => { - const response = await doormileAxios.get('/admin/competitor-branches'); - return response.data.data; +// Confirmed live: this resource paginates on page/limit, not the +// pageno/pagesize convention used everywhere else in the API โ€” and defaults +// to only 50 rows (there are 339+ competitor branches on a real tenant), so +// callers must page through it explicitly rather than trusting one fetch to +// return everything. Returns the full envelope (data/limit/page/total) so +// the caller can build pagination controls. +export const getCompetitorBranches = async (page, limit) => { + const response = await doormileAxios.get(`/admin/competitor-branches${buildQuery({ page, limit })}`); + return response.data; }; export const createCompetitorBranch = async (data) => { @@ -536,9 +544,11 @@ export const deleteCompetitorBranch = async (id) => { return response.data; }; -export const getCarrierPricing = async () => { - const response = await doormileAxios.get('/admin/carrier-pricing'); - return response.data.data; +// Same page/limit pagination convention as competitor-branches (confirmed +// live) โ€” 368+ rows on a real tenant, default page returns only 100. +export const getCarrierPricing = async (page, limit) => { + const response = await doormileAxios.get(`/admin/carrier-pricing${buildQuery({ page, limit })}`); + return response.data; }; export const createCarrierPricing = async (data) => { diff --git a/src/pages/nearle/clientPricing/clientPricing.js b/src/pages/nearle/clientPricing/clientPricing.js index 5f0b9db..31edb8f 100644 --- a/src/pages/nearle/clientPricing/clientPricing.js +++ b/src/pages/nearle/clientPricing/clientPricing.js @@ -168,7 +168,8 @@ const emptyForm = { priceperkm: '', handlingcharges: '', priority: 1, - status: 'active' + // Confirmed live: matches every real pricing row's casing ("Active"). + status: 'Active' }; // ==============================|| Pricing page ||============================== // @@ -207,24 +208,30 @@ const ClientsPricing = () => { const zoneName = (row) => row.applocation || locationMap.get(row.applocationid)?.locationname || (row.applocationid ? `Zone #${row.applocationid}` : 'โ€”'); + // getallpricing() takes no zone arg (see api.js) โ€” the LocationAutocomplete + // only ever changed the header subtitle text, not which rows were shown. + // Filter client-side instead; pricing rows already carry applocationid + // (used by zoneName above), so this is the same real field, just applied. + const zonePricing = useMemo(() => (appId ? pricing.filter((r) => r.applocationid === appId) : pricing), [pricing, appId]); + const rows = useMemo(() => { - if (!debouncedSearch) return pricing; + if (!debouncedSearch) return zonePricing; const q = debouncedSearch.toLowerCase().trim(); - return pricing.filter((row) => + return zonePricing.filter((row) => [tenantName(row), row.vehicletype, zoneName(row), String(row.pricingid)] .filter(Boolean) .some((field) => String(field).toLowerCase().includes(q)) ); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pricing, debouncedSearch, tenantMap, locationMap]); + }, [zonePricing, debouncedSearch, tenantMap, locationMap]); const stats = useMemo(() => { - const total = pricing.length; - const tenantsPriced = new Set(pricing.map((r) => r.tenantid).filter(Boolean)).size; - const avgBase = total ? pricing.reduce((sum, r) => sum + (Number(r.baseprice) || 0), 0) / total : 0; - const activeCount = pricing.filter((r) => String(r.status).toLowerCase() === 'active').length; + const total = zonePricing.length; + const tenantsPriced = new Set(zonePricing.map((r) => r.tenantid).filter(Boolean)).size; + const avgBase = total ? zonePricing.reduce((sum, r) => sum + (Number(r.baseprice) || 0), 0) / total : 0; + const activeCount = zonePricing.filter((r) => String(r.status).toLowerCase() === 'active').length; return { total, tenantsPriced, avgBase, activeCount }; - }, [pricing]); + }, [zonePricing]); const KPI_META = [ { key: 'total', label: 'Total Pricing Rules', color: BRAND, icon: MdOutlineLocalOffer, value: stats.total }, @@ -305,7 +312,7 @@ const ClientsPricing = () => { priceperkm: row.priceperkm ?? '', handlingcharges: row.handlingcharges ?? '', priority: row.priority ?? 1, - status: row.status || 'active' + status: row.status || 'Active' }); setDialogOpen(true); }; @@ -326,6 +333,8 @@ const ClientsPricing = () => { return; } // POST/PUT /admin/pricing body per express-console-api.md. + // effectivefrom is required and confirmed live to reject a bare + // YYYY-MM-DD date ("invalid request body") โ€” needs a full ISO datetime. const data = { tenantid: form.tenant.tenantid, applocationid: form.location?.applocationid, @@ -336,6 +345,7 @@ const ClientsPricing = () => { basedistance: Number(form.basedistance) || 0, priceperkm: Number(form.priceperkm), handlingcharges: Number(form.handlingcharges) || 0, + effectivefrom: new Date().toISOString(), currency: 'INR', priority: Number(form.priority) || 1, status: form.status @@ -765,8 +775,8 @@ const ClientsPricing = () => { Status diff --git a/src/pages/nearle/clients/Tenants.js b/src/pages/nearle/clients/Tenants.js index 2663a8d..a87e570 100644 --- a/src/pages/nearle/clients/Tenants.js +++ b/src/pages/nearle/clients/Tenants.js @@ -79,7 +79,7 @@ import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import PageHeader from 'components/nearle_components/PageHeader'; import StatCard from 'components/nearle_components/StatCard'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { getalltenants, gettenantsummary } from 'pages/api/api'; +import { getalltenants } from 'pages/api/api'; import { getAdminPricing, getDoormilePricing, @@ -440,7 +440,11 @@ const TenantCustomersDialog = ({ tenant, open, onClose }) => { id: tenantCustId(row), firstname: row.firstname || '', lastname: row.lastname || '', - phone: row.phone || '', + // GET/POST /admin/tenantcustomers has a write/read asymmetry confirmed + // live: POST accepts `phone`, but the stored/returned field is + // `contactno`. The form field itself stays named `phone` (matching + // what create/update accept) โ€” only the read source differs. + phone: row.contactno || '', email: row.email || '' }); setFormOpen(true); @@ -470,7 +474,7 @@ const TenantCustomersDialog = ({ tenant, open, onClose }) => { {c.firstname} {c.lastname} - {[c.phone, c.email].filter(Boolean).join(' ยท ')} + {[c.contactno, c.email].filter(Boolean).join(' ยท ')} @@ -580,11 +584,9 @@ const Clients1 = () => { setPage(0); }; const handleChange1 = (event, newValue1) => { - console.log('newValue1', newValue1); setValue1(newValue1); }; const handleChange2 = (event, newValue2) => { - console.log('newValue2', newValue2); setValue2(newValue2); }; @@ -600,9 +602,6 @@ const Clients1 = () => { const handleCollapseToggle2 = (rowIndex) => { setOpenRowIndex2((prevIndex) => (prevIndex === rowIndex ? null : rowIndex)); }; - useEffect(() => { - console.log('selectedCustomer', selectedCustomer); - }, [selectedCustomer]); useEffect(() => { setOpenRowIndex1(-1); setOpenRowIndex2(-1); @@ -700,15 +699,38 @@ const Clients1 = () => { queryFn: getalltenants }); - // ==============================|| gettenantsummary ||============================== // - const { - data: summaryData = [], - isLoading: summaryDataIsLoading, - refetch: summaryDataRefetch - } = useQuery({ - queryKey: ['gettenantsummary', appId, status], - queryFn: gettenantsummary - }); + // No tenant-tab-counts endpoint in the new API (gettenantsummary always + // returns null) โ€” derive the Active/Pending/Inactive counts client-side + // from the full tenant list instead, which getalltenants already fetches. + const tenantStatusCounts = React.useMemo(() => { + const counts = { active: 0, pending: 0, inactive: 0 }; + (tenantList || []).forEach((t) => { + const s = String(t.status || '').toLowerCase(); + if (s === 'active') counts.active += 1; + else if (s === 'pending') counts.pending += 1; + else if (s === 'inactive') counts.inactive += 1; + }); + return counts; + }, [tenantList]); + + // getalltenants() takes no filter args (see api.js) โ€” the status tabs and + // search box only ever changed the query-cache key, never the actual + // request, so every tab showed the exact same unfiltered list with just a + // relabeled status badge. Filter the already-fetched list client-side + // instead, matching the same real `status` field tenantStatusCounts uses. + const filteredTenantList = React.useMemo(() => { + const activeStatus = STATUS_TABS[value0]?.status; + const q = debouncedSearch.trim().toLowerCase(); + return (tenantList || []).filter((t) => { + if (String(t.status || '').toLowerCase() !== activeStatus) return false; + if (!q) return true; + return ( + t.tenantname?.toLowerCase().includes(q) || + t.primaryemail?.toLowerCase().includes(q) || + t.primarycontact?.includes(debouncedSearch) + ); + }); + }, [tenantList, value0, debouncedSearch]); /* ============================================= || fetchclientpricelist || ============================================= */ @@ -718,7 +740,7 @@ const Clients1 = () => { const pricing = await getAdminPricing(); setClientpricelist((pricing || []).filter((p) => p.tenantid === selectedTenid)); } catch (err) { - console.log(err); + // swallow } }; useEffect(() => { @@ -731,7 +753,7 @@ const Clients1 = () => { const pricing = await getAdminPricing(); setTenanatPricing((pricing || []).filter((p) => p.tenantid === id)); } catch (error) { - console.log('fetchTenanatPricing', error); + // swallow } }; useEffect(() => { @@ -760,10 +782,8 @@ const Clients1 = () => { 2000 ); getalltenantsRefetch(); - summaryDataRefetch(); } } catch (err) { - console.log(err); opentoast(err.message, 'error', 1500); } finally { setisloader(false); @@ -776,7 +796,7 @@ const Clients1 = () => { const pricing = await getDoormilePricing(); setAppPricing((pricing || []).filter((p) => !id || p.applocationid === id)); } catch (error) { - console.log('appPricingRes', error); + // swallow } }; /* ============================================= || createpricing || ============================================= */ @@ -793,10 +813,14 @@ const Clients1 = () => { priceperkg: +selectedPricing.priceperkg || 0, basedistance: +selectedPricing.basedistance || 0, handlingcharges: +selectedPricing.handlingcharges || 0, - effectivefrom: dayjs().format('YYYY-MM-DD'), + // Confirmed live: POST /admin/pricing rejects a bare YYYY-MM-DD date + // for effectivefrom ("invalid request body") โ€” needs a full ISO + // datetime. Status also confirmed to need title case ("Active"), + // matching every real pricing row. + effectivefrom: dayjs().toISOString(), currency: 'INR', priority: 1, - status: 'active' + status: 'Active' }); if (res.success) { enqueueSnackbar('Price Created Successfully', { @@ -812,7 +836,6 @@ const Clients1 = () => { opentoast(res.message || 'Failed to save pricing', 'error', 2000); } } catch (err) { - console.log(err); opentoast(err.response?.data?.message || err.message || 'Failed to save pricing', 'error', 2000); } }; @@ -826,9 +849,7 @@ const Clients1 = () => { opentoast(updateRes.message || 'Update Failed', 'error', 1500); } getalltenantsRefetch(); - summaryDataRefetch(); } catch (err) { - console.log('updateClient', err); opentoast(err.response?.data?.message || err.message || 'Update Failed', 'error', 1500); } }; @@ -837,7 +858,7 @@ const Clients1 = () => { return ( <> - {(getalltenantsIsLoading || summaryDataIsLoading || isloader) && } + {(getalltenantsIsLoading || isloader) && } {/* ============================================= || Header | ============================================= */} { {KPI_META.map((item, idx) => { const Icon = item.icon; - const value = summaryData?.[item.countKey] ?? 0; + const value = tenantStatusCounts[item.countKey] ?? 0; return ( { value={value ?? 0} icon={} color={idx === 0 ? '#662582' : item.color} - loading={summaryDataIsLoading} + loading={getalltenantsIsLoading} /> @@ -925,7 +946,7 @@ const Clients1 = () => { const meta = STATUS_META[t.status]; const Icon = meta.icon; const active = value0 === idx; - const count = summaryData?.[t.countKey] ?? 0; + const count = tenantStatusCounts[t.countKey] ?? 0; return ( { border: 'none' }} > - {summaryDataIsLoading ? : count} + {getalltenantsIsLoading ? : count} ); @@ -1064,14 +1085,14 @@ const Clients1 = () => { Status Client Contact - Address + Delivery OTP Actions {getalltenantsIsLoading && } - {tenantList?.length == 0 && !isloader ? ( + {filteredTenantList.length == 0 && !isloader ? ( @@ -1088,9 +1109,9 @@ const Clients1 = () => { ) : ( - tenantList?.map((row, index) => { - const rowStatusKey = (value0 === 0 ? 'active' : value0 === 1 ? 'pending' : 'inactive'); - const rowStatusMeta = STATUS_META[rowStatusKey]; + filteredTenantList.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => { + const rowStatusKey = String(row.status || '').toLowerCase(); + const rowStatusMeta = STATUS_META[rowStatusKey] || STATUS_META.active; const RowStatusIcon = rowStatusMeta.icon; return ( @@ -1162,20 +1183,9 @@ const Clients1 = () => { - - - {row.address || 'โ€”'} - - + + {row.requiredeliveryotp ? 'Yes' : 'No'} + @@ -1217,7 +1227,7 @@ const Clients1 = () => { setDialogopen(true); setSelectedtenid(row.tenantid); setAppId(row.applocationid); - getAppPricing(row.applolcationid); + getAppPricing(row.applocationid); }} > @@ -1407,7 +1417,7 @@ const Clients1 = () => { - { /> - {row.firstname} + {row.status || 'โ€”'} - Contact Person + Status @@ -1461,121 +1471,26 @@ const Clients1 = () => { - + {row.requiredeliveryotp ? ( + + ) : ( + + )} - {row.address} + {row.requiredeliveryotp ? 'Yes' : 'No'} - Address - - - - - - {/* - - - - - - {row.subcategoryname} - - Category - - - - - - - - {row.suburb} - - - Location - - - - */} - - - - - - - - {row.city} - - City - - - - - - - - {row.postcode} - - - PostCode - - - - - - - - - - - {row.latitude} - - Latitude - - - - - - - - {row.longitude} - - - Longitude + Delivery OTP Required @@ -1681,7 +1596,7 @@ const Clients1 = () => { flexDirection: 'row' }} onClick={() => { - getAppPricing(selectedCustomer.applolcationid); + getAppPricing(selectedCustomer.applocationid); }} /> )} @@ -2105,7 +2020,7 @@ const Clients1 = () => { )} - {tenantList?.length == 0 && !isloader ? ( + {filteredTenantList.length == 0 && !isloader ? ( @@ -2118,9 +2033,9 @@ const Clients1 = () => { ) : ( - tenantList?.map((row, index) => { - const rowStatusKey = value0 === 0 ? 'active' : value0 === 1 ? 'pending' : 'inactive'; - const rowStatusMeta = STATUS_META[rowStatusKey]; + filteredTenantList.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage).map((row, index) => { + const rowStatusKey = String(row.status || '').toLowerCase(); + const rowStatusMeta = STATUS_META[rowStatusKey] || STATUS_META.active; const RowStatusIcon = rowStatusMeta.icon; const expanded = openRowIndex1 === index; return ( @@ -2199,7 +2114,7 @@ const Clients1 = () => { setDialogopen(true); setSelectedtenid(row.tenantid); setAppId(row.applocationid); - getAppPricing(row.applolcationid); + getAppPricing(row.applocationid); }} > @@ -2280,7 +2195,7 @@ const Clients1 = () => { setSelectedCustomer(row); setSelectedtenid(row.tenantid); setAppId(row.applocationid); - getAppPricing(row.applolcationid); + getAppPricing(row.applocationid); setDialogopen(true); }} > @@ -2295,21 +2210,13 @@ const Clients1 = () => { - - - {row.address || 'โ€”'} - - + {expanded && ( - - - - - + )} @@ -2321,13 +2228,13 @@ const Clients1 = () => { )} {/* ============================================= || Pagination| ============================================= */} - {!searchword && tenantList?.length > 0 && ( + {!searchword && filteredTenantList.length > 0 && ( <> { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const navigate = useNavigate(); + + const loggedInTenantId = localStorage.getItem('tenantid'); + const isStaffLogin = !loggedInTenantId || loggedInTenantId === '0'; + + const [appId, setAppId] = useState(0); + const [locaName, setLocoName] = useState('Select Location'); + const [tid, setTid] = useState(isStaffLogin ? '' : loggedInTenantId); + + const [firstname, setFirstname] = useState(''); + const [lastname, setLastname] = useState(''); + const [phone, setPhone] = useState(''); + const [email, setEmail] = useState(''); + const [doorno, setDoorno] = useState(''); + const [inputValue2, setInputValue2] = useState(''); + const [landmark, setLandmark] = useState(''); + const [pickCust, setPickCust] = useState({}); + const [startPoint, setStartPoint] = useState({ latitude: 0, longitude: 0 }); + const [appLocaLat, setAppLocaLat] = useState(); + const [appLocaLng, setAppLocaLng] = useState(); + + const { data: tenantList = [], isLoading: tenantListLoading } = useQuery({ + queryKey: ['admin-tenants-picker'], + queryFn: getAdminTenants, + enabled: isStaffLogin + }); + + // LocationAutocomplete only reports back applocationid/locationname โ€” it + // caches the full hub list (with lat/lng) in localStorage('applocations') + // as a side effect, so pull the coordinates for the address-search bias + // from there rather than duplicating the GET /admin/hubs call. + useEffect(() => { + if (!appId) return; + try { + const hubs = JSON.parse(localStorage.getItem('applocations') || '[]'); + const hub = hubs.find((h) => h.applocationid === appId); + if (hub?.latitude) { + setAppLocaLat(hub.latitude); + setAppLocaLng(hub.longitude); + } + } catch (err) { + // bias is a soft nicety โ€” ignore a bad cache + } + }, [appId]); + + const handlePlaceSelected = (place) => { + setStartPoint({ latitude: place.geometry.location.lat(), longitude: place.geometry.location.lng() }); + const parsed = { suburb: '', city: '', state: '', postcode: '' }; + place.address_components.forEach((component) => { + component.types.forEach((type) => { + switch (type) { + case 'sublocality_level_1': + case 'sublocality': + parsed.suburb = component.long_name; + break; + case 'locality': + parsed.city = component.long_name; + break; + case 'administrative_area_level_1': + parsed.state = component.long_name; + break; + case 'postal_code': + parsed.postcode = component.long_name; + break; + } + }); + }); + setPickCust({ + address: place.formatted_address, + suburb: parsed.suburb, + city: parsed.city, + state: parsed.state, + postcode: parsed.postcode, + latitude: place.geometry.location.lat(), + longitude: place.geometry.location.lng() + }); + }; + + const opentoast = (message) => { + enqueueSnackbar(message, { + variant: 'error', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + }; + + const createCustomerMutation = useMutation({ + mutationFn: (payload) => createTenantCustomer(payload), + onSuccess: (res) => { + if (res.success) { + enqueueSnackbar('Customer created successfully', { + variant: 'success', + anchorOrigin: { vertical: 'top', horizontal: 'right' }, + autoHideDuration: 2000 + }); + navigate('/doormile/customers'); + } else { + opentoast(res.message || 'Could not create customer'); + } + }, + onError: (err) => { + opentoast(err.response?.data?.message || err.message || 'Failed to create customer'); + } + }); + + const createprofile = () => { + if (isStaffLogin && !tid) { + opentoast('Select Tenant'); + } else if (!firstname) { + opentoast('Enter Name'); + } else if (!phone || phone.length !== 10) { + opentoast('Enter a valid 10-digit Mobile Number'); + } else { + createCustomerMutation.mutate({ + firstname, + lastname, + phone, + email, + ...(isStaffLogin && tid ? { tenantid: +tid } : {}) + }); + } + }; + + return ( + <> + {createCustomerMutation.isPending && } + + + + + + + + Create Customer + + + + + + + + + {/* ===================================================== || Location & Client || ===================================================== */} + + + Location + + + + {isStaffLogin && ( + + + Choose Client + option?.tenantname || ''} + renderInput={(params) => } + onChange={(e, val) => setTid(val ? val.tenantid : '')} + /> + + + )} + + {/* ===================================================== || Name / Phone / Email || ===================================================== */} + + + Name + setFirstname(e.target.value)} + value={firstname} + autoComplete="off" + /> + + + + + Last Name + setLastname(e.target.value)} + value={lastname} + autoComplete="off" + /> + + + + + Phone Number + + + { + if (e.target.value.toString().length <= 10) { + setPhone(e.target.value); + } + }} + value={phone} + autoComplete="off" + /> + + + + + + Email + setEmail(e.target.value)} + value={email} + autoComplete="off" + /> + + + + {/* ===================================================== || Door No || ===================================================== */} + + + Door No + setDoorno(e.target.value)} + value={doorno} + autoComplete="off" + /> + + + + {/* ===================================================== || Address section โ€” collected, not persisted || ===================================================== */} + + + The backend doesn't store an address against a customer yet โ€” these fields aren't required and won't be saved on submit. + + + + + Address + setInputValue2(text)} + onPlaceSelected={handlePlaceSelected} + bias={{ lat: appLocaLat, lng: appLocaLng }} + TextFieldProps={{ + variant: 'outlined', + InputProps: { + endAdornment: ( + { + setInputValue2(''); + setPickCust({}); + setStartPoint({ latitude: 0, longitude: 0 }); + }} + size="small" + > + + + ) + } + }} + /> + + + + + Location + setPickCust((p) => ({ ...p, suburb: e.target.value }))} + value={pickCust.suburb || ''} + autoComplete="off" + /> + + + + + City + setPickCust((p) => ({ ...p, city: e.target.value }))} + value={pickCust.city || ''} + autoComplete="off" + /> + + + + + State + setPickCust((p) => ({ ...p, state: e.target.value }))} + value={pickCust.state || ''} + autoComplete="off" + /> + + + + + Post Code + setPickCust((p) => ({ ...p, postcode: e.target.value }))} + value={pickCust.postcode || ''} + autoComplete="off" + /> + + + + + Landmark + setLandmark(e.target.value)} value={landmark} autoComplete="off" /> + + + + + Latitude + + + + + + Longitude + + + + + + + + + + + + + + + ); +}; + +export default Createcustomer; diff --git a/src/pages/nearle/clients/createclient.js b/src/pages/nearle/clients/createclient.js index 87ae28a..8cf8186 100644 --- a/src/pages/nearle/clients/createclient.js +++ b/src/pages/nearle/clients/createclient.js @@ -36,7 +36,11 @@ const Createclient = () => { const [tenantname, setTenantname] = useState(''); const [primaryemail, setPrimaryemail] = useState(''); const [primarycontact, setPrimarycontact] = useState(''); - const [status, setStatus] = useState('active'); + // Confirmed live: POST /admin/tenants doesn't reject lowercase status, but + // stores it exactly as sent โ€” every real tenant uses "Active" (title + // case), so a lowercase one would be inconsistent with the rest of the + // data even though the request itself succeeds. + const [status, setStatus] = useState('Active'); const [requiredeliveryotp, setRequiredeliveryotp] = useState(false); const opentoast = (message) => { @@ -137,8 +141,8 @@ const Createclient = () => { Status diff --git a/src/pages/nearle/competitiveIntel/competitiveIntel.js b/src/pages/nearle/competitiveIntel/competitiveIntel.js index 0b0d5f9..2cd1a59 100644 --- a/src/pages/nearle/competitiveIntel/competitiveIntel.js +++ b/src/pages/nearle/competitiveIntel/competitiveIntel.js @@ -10,6 +10,7 @@ import { Grid, IconButton, InputLabel, + Pagination, Paper, Stack, Table, @@ -40,13 +41,21 @@ import { import { enqueueSnackbar } from 'notistack'; // ============================================================================ -// Neither /admin/competitor-branches nor /admin/carrier-pricing document a -// request/response body anywhere โ€” express-console-api.md lists only the -// routes. The fields below (branchname/city/address for branches, -// carriername/vehicletype/baseprice/priceperkm for pricing) are a best-effort -// guess following the naming conventions used elsewhere in this API and are -// UNVERIFIED โ€” expect the create/edit calls here to need correction once a -// real request/response has been seen. +// Field names confirmed live (create+verify+delete round-trip against +// production, per an explicit one-off testing pass) โ€” the original guesses +// here (branchname/competitorname/city for branches, carriername/ +// vehicletype/baseprice/priceperkm for pricing) were entirely wrong and have +// been replaced with the real shape: +// +// competitor-branches: { id, company, area, phone, plus_code, address, +// pincodes, rate_per_kg, offers_pickup, offers_drop, time_in_days, +// packing_charge, frequency, created_at, updated_at } +// carrier-pricing: { id, company, weight_slab, service_type, zone, rate, +// delivery_time, created_at, updated_at } +// +// rate_per_kg/rate are free-text strings in the real data (e.g. "600", +// "25-35", "Not Answered", ".") โ€” not clean numbers โ€” so they're kept as +// plain text fields, not Number-cast. // ============================================================================ const DT = { @@ -67,24 +76,50 @@ const TABS = [ { key: 'pricing', label: 'Carrier Pricing' } ]; -const emptyBranch = { id: null, branchname: '', competitorname: '', city: '', address: '' }; -const emptyCarrier = { id: null, carriername: '', vehicletype: '', baseprice: '', priceperkm: '' }; +const emptyBranch = { + id: null, + company: '', + area: '', + phone: '', + address: '', + pincodes: '', + rate_per_kg: '', + offers_pickup: '', + offers_drop: '', + time_in_days: '', + packing_charge: '' +}; +const emptyCarrier = { id: null, company: '', weight_slab: '', service_type: '', zone: '', rate: '', delivery_time: '' }; +const PAGE_SIZE = 50; const CompetitiveIntel = () => { const queryClient = useQueryClient(); const [tab, setTab] = useState('branches'); + const [page, setPage] = useState(1); const [dialogOpen, setDialogOpen] = useState(false); const [branchForm, setBranchForm] = useState(emptyBranch); const [carrierForm, setCarrierForm] = useState(emptyCarrier); - const { data: branches = [], isLoading: branchesLoading } = useQuery({ - queryKey: ['competitor-branches'], - queryFn: getCompetitorBranches + const changeTab = (t) => { + setTab(t); + setPage(1); + }; + + const { data: branchesRes, isLoading: branchesLoading } = useQuery({ + queryKey: ['competitor-branches', page], + queryFn: () => getCompetitorBranches(page, PAGE_SIZE), + enabled: tab === 'branches', + keepPreviousData: true }); - const { data: carrierPricing = [], isLoading: carrierLoading } = useQuery({ - queryKey: ['carrier-pricing'], - queryFn: getCarrierPricing + const branches = branchesRes?.data || []; + + const { data: carrierRes, isLoading: carrierLoading } = useQuery({ + queryKey: ['carrier-pricing', page], + queryFn: () => getCarrierPricing(page, PAGE_SIZE), + enabled: tab === 'pricing', + keepPreviousData: true }); + const carrierPricing = carrierRes?.data || []; const opentoast = (message, variant = 'error') => enqueueSnackbar(message, { variant, anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 2000 }); @@ -139,8 +174,6 @@ const CompetitiveIntel = () => { onError: (err) => opentoast(err.response?.data?.message || err.message || 'Failed to delete carrier pricing') }); - const idOf = (row) => row.id ?? row.competitorbranchid ?? row.carrierpricingid; - const openCreate = () => { if (tab === 'branches') setBranchForm(emptyBranch); else setCarrierForm(emptyCarrier); @@ -150,19 +183,27 @@ const CompetitiveIntel = () => { const openEdit = (row) => { if (tab === 'branches') { setBranchForm({ - id: idOf(row), - branchname: row.branchname || '', - competitorname: row.competitorname || '', - city: row.city || '', - address: row.address || '' + id: row.id, + company: row.company || '', + area: row.area || '', + phone: row.phone || '', + address: row.address || '', + pincodes: row.pincodes || '', + rate_per_kg: row.rate_per_kg || '', + offers_pickup: row.offers_pickup || '', + offers_drop: row.offers_drop || '', + time_in_days: row.time_in_days || '', + packing_charge: row.packing_charge || '' }); } else { setCarrierForm({ - id: idOf(row), - carriername: row.carriername || '', - vehicletype: row.vehicletype || '', - baseprice: row.baseprice ?? '', - priceperkm: row.priceperkm ?? '' + id: row.id, + company: row.company || '', + weight_slab: row.weight_slab || '', + service_type: row.service_type || '', + zone: row.zone || '', + rate: row.rate || '', + delivery_time: row.delivery_time || '' }); } setDialogOpen(true); @@ -170,30 +211,32 @@ const CompetitiveIntel = () => { const handleDelete = (row) => { if (!window.confirm('Delete this record?')) return; - if (tab === 'branches') branchDeleteMutation.mutate(idOf(row)); - else carrierDeleteMutation.mutate(idOf(row)); + if (tab === 'branches') branchDeleteMutation.mutate(row.id); + else carrierDeleteMutation.mutate(row.id); }; const handleSave = () => { if (tab === 'branches') { - if (!branchForm.branchname) { - opentoast('Fill Branch Name'); + if (!branchForm.company) { + opentoast('Fill Company'); return; } const { id, ...data } = branchForm; branchMutation.mutate({ id, data }); } else { - if (!carrierForm.carriername || !carrierForm.baseprice) { - opentoast('Fill Carrier Name and Base Price'); + if (!carrierForm.company || !carrierForm.weight_slab) { + opentoast('Fill Company and Weight Slab'); return; } const { id, ...data } = carrierForm; - carrierMutation.mutate({ id, data: { ...data, baseprice: Number(data.baseprice) || 0, priceperkm: Number(data.priceperkm) || 0 } }); + carrierMutation.mutate({ id, data }); } }; const rows = tab === 'branches' ? branches : carrierPricing; const isLoading = tab === 'branches' ? branchesLoading : carrierLoading; + const total = (tab === 'branches' ? branchesRes?.total : carrierRes?.total) || 0; + const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE)); return ( <> @@ -214,7 +257,7 @@ const CompetitiveIntel = () => { {TABS.map((t) => ( setTab(t.key)} + onClick={() => changeTab(t.key)} sx={{ px: 2, py: 0.75, @@ -232,26 +275,56 @@ const CompetitiveIntel = () => { ))} - - - + + {total} total ยท page {page} of {pageCount} + + + + +
{tab === 'branches' ? ( - + # - Branch Name - Competitor - City + Company + Area + Phone Address + Rate/Kg Actions ) : ( - + # - Carrier - Vehicle Type - Base Price - Price/Km + Company + Weight Slab + Zone + Rate Actions )} @@ -259,7 +332,7 @@ const CompetitiveIntel = () => { {rows.length === 0 && !isLoading ? ( - + @@ -272,21 +345,29 @@ const CompetitiveIntel = () => { ) : ( rows.map((row, index) => ( - - {index + 1} + + {(page - 1) * PAGE_SIZE + index + 1} {tab === 'branches' ? ( <> - {row.branchname || 'โ€”'} - {row.competitorname || 'โ€”'} - {row.city || 'โ€”'} - {row.address || 'โ€”'} + {row.company || 'โ€”'} + {row.area || 'โ€”'} + {row.phone || 'โ€”'} + + + {row.address || 'โ€”'} + + + {row.rate_per_kg || 'โ€”'} ) : ( <> - {row.carriername || 'โ€”'} - {row.vehicletype || 'โ€”'} - {row.baseprice ?? 'โ€”'} - {row.priceperkm ?? 'โ€”'} + {row.company || 'โ€”'} + {row.weight_slab || 'โ€”'} + {row.zone || 'โ€”'} + {row.rate || 'โ€”'} )} @@ -311,62 +392,149 @@ const CompetitiveIntel = () => { - setDialogOpen(false)} maxWidth="xs" fullWidth> + {pageCount > 1 && ( + + setPage(value)} color="primary" shape="rounded" /> + + )} + + setDialogOpen(false)} maxWidth="sm" fullWidth> {tab === 'branches' ? (branchForm.id ? 'Edit Branch' : 'New Competitor Branch') : carrierForm.id ? 'Edit Carrier Rate' : 'New Carrier Rate'} {tab === 'branches' ? ( - + - Branch Name - setBranchForm((f) => ({ ...f, branchname: e.target.value }))} /> - - - - - Competitor Name - setBranchForm((f) => ({ ...f, competitorname: e.target.value }))} /> + Company + setBranchForm((f) => ({ ...f, company: e.target.value }))} /> - City - setBranchForm((f) => ({ ...f, city: e.target.value }))} /> + Area + setBranchForm((f) => ({ ...f, area: e.target.value }))} /> + + Phone + setBranchForm((f) => ({ ...f, phone: e.target.value }))} /> + + + + + Pincodes + setBranchForm((f) => ({ ...f, pincodes: e.target.value }))} /> + + + Address setBranchForm((f) => ({ ...f, address: e.target.value }))} /> + + + Rate / Kg + setBranchForm((f) => ({ ...f, rate_per_kg: e.target.value }))} + placeholder="e.g. 600, or a note like 'Not Answered'" + /> + + + + + Time (days) + setBranchForm((f) => ({ ...f, time_in_days: e.target.value }))} /> + + + + + Offers Pickup + setBranchForm((f) => ({ ...f, offers_pickup: e.target.value }))} + placeholder="yes / no" + /> + + + + + Offers Drop + setBranchForm((f) => ({ ...f, offers_drop: e.target.value }))} + placeholder="yes / no" + /> + + + + + Packing Charge + setBranchForm((f) => ({ ...f, packing_charge: e.target.value }))} + /> + + ) : ( - Carrier Name - setCarrierForm((f) => ({ ...f, carriername: e.target.value }))} /> + Company + setCarrierForm((f) => ({ ...f, company: e.target.value }))} /> + + + + + Weight Slab + setCarrierForm((f) => ({ ...f, weight_slab: e.target.value }))} + placeholder="e.g. 500g - 1kg" + /> + + + + + Service Type + setCarrierForm((f) => ({ ...f, service_type: e.target.value }))} + /> + + + + + Zone + setCarrierForm((f) => ({ ...f, zone: e.target.value }))} + placeholder="e.g. Local / Within City" + /> + + + + + Rate + setCarrierForm((f) => ({ ...f, rate: e.target.value }))} + placeholder="e.g. 120-200" + /> - Vehicle Type - setCarrierForm((f) => ({ ...f, vehicletype: e.target.value }))} /> - - - - - Base Price - setCarrierForm((f) => ({ ...f, baseprice: e.target.value }))} /> - - - - - Price/Km - setCarrierForm((f) => ({ ...f, priceperkm: e.target.value }))} /> + Delivery Time + setCarrierForm((f) => ({ ...f, delivery_time: e.target.value }))} + /> diff --git a/src/pages/nearle/customers/customers.js b/src/pages/nearle/customers/customers.js index c2856f8..2244f8f 100644 --- a/src/pages/nearle/customers/customers.js +++ b/src/pages/nearle/customers/customers.js @@ -24,8 +24,20 @@ import { useMediaQuery, useTheme } from '@mui/material'; -import { MdPeopleAlt, MdOutlinePeopleAlt, MdEdit, MdPhone, MdMail, MdOutlineMailOutline, MdOutlinePhoneAndroid } from 'react-icons/md'; +import { + MdPeopleAlt, + MdOutlinePeopleAlt, + MdEdit, + MdPhone, + MdMail, + MdOutlineMailOutline, + MdOutlinePhoneAndroid, + MdPersonAddAlt1, + MdOutlineReceiptLong +} from 'react-icons/md'; +import { FaRegEdit } from 'react-icons/fa'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useNavigate } from 'react-router'; import Loader from 'components/Loader'; import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; @@ -63,6 +75,8 @@ const soft = (c) => a(c, '18'); const ring = (c) => a(c, '26'); const edge = (c) => a(c, '55'); const BRAND = '#C01227'; +const BRAND_LIGHT = '#D25463'; +const tint = (c) => a(c, '08'); const AccentAvatar = ({ color, size = 24, children }) => ( {children} @@ -77,6 +91,7 @@ const custBookings = (row) => row.totalbookings ?? 0; const Customers = () => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const navigate = useNavigate(); const queryClient = useQueryClient(); const [searchword, setSearchword] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); @@ -96,7 +111,8 @@ const Customers = () => { const stats = useMemo(() => { const total = customers.length; const withEmail = customers.filter((c) => c.email).length; - return { total, withEmail }; + const totalBookings = customers.reduce((sum, c) => sum + (Number(c.totalbookings) || 0), 0); + return { total, withEmail, totalBookings }; }, [customers]); const opentoast = (message, variant = 'error') => @@ -129,7 +145,23 @@ const Customers = () => { <> {isLoading && } - + + + + } + /> @@ -138,6 +170,9 @@ const Customers = () => { } color="#0ea5e9" /> + + } color="#10b981" /> + { }} > - - {customers.length} total ยท {rows.length} shown - + + + + + + + Directory + + + {customers.length} total ยท {rows.length} shown + + + { sx={{ m: 0, width: '100%', - borderRadius: DT.radiusField + 'px', - bgcolor: DT.surface, - '& fieldset': { borderColor: DT.borderSubtle, borderWidth: 1 }, - '&:hover fieldset': { borderColor: DT.borderHover }, - '&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 1.5 }, + borderRadius: 999, + bgcolor: tint(BRAND), + '& fieldset': { borderColor: edge(BRAND), borderWidth: 1.5 }, + '&:hover fieldset': { borderColor: BRAND }, + '&.Mui-focused fieldset': { borderColor: BRAND, borderWidth: 2 }, '&.Mui-focused': { boxShadow: `0 0 0 3px ${ring(BRAND)}` } }} /> @@ -207,15 +252,20 @@ const Customers = () => { accent={BRAND} header={ - + - - {custName(row)} - + + + {custName(row)} + + + ID #{custId(row) ?? 'โ€”'} + + - openEdit(row)} sx={{ color: BRAND }}> + openEdit(row)} sx={{ color: BRAND, flexShrink: 0 }}> @@ -256,9 +306,8 @@ const Customers = () => { }} > # - Name - Phone - Email + Customer + Contact Bookings Actions @@ -267,7 +316,7 @@ const Customers = () => { {isLoading && } {rows.length === 0 && !isLoading ? ( - + @@ -284,25 +333,42 @@ const Customers = () => { key={custId(row) || index} sx={{ '& td': { borderBottom: `1px solid ${DT.divider}` }, '&:hover': { backgroundColor: DT.surfaceAlt } }} > - {index + 1} + + + {String(index + 1).padStart(2, '0')} + + - - {custName(row)} - + + + {custName(row)} + + + ID #{custId(row) ?? 'โ€”'} + + - - {custPhone(row)} - - - - - {custEmail(row)} + + + + + {custPhone(row)} + + + {row.email && ( + + + + {row.email} + + + )} {custBookings(row)} @@ -322,31 +388,69 @@ const Customers = () => { )} - setEditRow(null)} maxWidth="xs" fullWidth> - Edit Customer + setEditRow(null)} maxWidth="xs" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}> + + + + + + + + Customer + + + Edit {editRow ? custName(editRow) : 'Customer'} + + + + - + Name - setForm((f) => ({ ...f, name: e.target.value }))} /> + setForm((f) => ({ ...f, name: e.target.value }))} /> - Phone - setForm((f) => ({ ...f, phone: e.target.value }))} - InputProps={{ startAdornment: }} - /> + Phone Number + + + setForm((f) => ({ ...f, phone: e.target.value.replace(/\D/g, '') }))} + InputProps={{ startAdornment: }} + /> + Email - setForm((f) => ({ ...f, email: e.target.value }))} /> + setForm((f) => ({ ...f, email: e.target.value }))} + InputProps={{ startAdornment: }} + /> - - + diff --git a/src/pages/nearle/deliveries/deliveries.js b/src/pages/nearle/deliveries/deliveries.js index 41ace71..1812722 100644 --- a/src/pages/nearle/deliveries/deliveries.js +++ b/src/pages/nearle/deliveries/deliveries.js @@ -329,7 +329,6 @@ const Deliveries = () => { const handleMenuOpen = (event, row) => { setSelectedRow(row); - console.log('selectedRow', row); setMenuAnchorEl(event.currentTarget); }; @@ -346,7 +345,6 @@ const Deliveries = () => { opentoast('Delivery Cancelled Successfully', 'success'); setCancelDeliveryOpen(false); fetchCountRefetch(); // Refresh count data - fetchDeliveriesRefetch(); // Refresh deliveries countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips }, onError: (error) => { @@ -357,14 +355,12 @@ const Deliveries = () => { // ==============================|| cancelridernotification ||============================== // const cancelridernotification = async () => { - console.log('cancelridernotification', selectedRow); try { // /admin/milers/:id/notify keys off milerprofileid, not the userid // stored on the booking (confirmed live) โ€” see fetchDeliveries in api.js. return await notifyMiler(selectedRow.milerprofileid, 'DoormileXpress', `${selectedRow.orderid} have been Cancelled`); } catch (err) { opentoast(err.message, 'error', 2000); - console.log(err); } }; @@ -425,7 +421,6 @@ const Deliveries = () => { opentoast('Rider Changed Successfully', 'success'); } fetchCountRefetch(); // Refresh count data - fetchDeliveriesRefetch(); // Refresh deliveries countSourceRefetch(); // Refresh the all-statuses dataset feeding table + chips // notifyMiler wants a miler profile id, not an FCM token โ€” the new API // looks the device token up server-side. selectedRider comes straight @@ -507,7 +502,6 @@ const Deliveries = () => { setCurrentStatus('cancelled'); setTotalCount(countData?.cancelLength); } - console.log(i); setSearchword(''); }; @@ -515,31 +509,6 @@ const Deliveries = () => { setOpen(false); }; - /* ============================================= || fetchDeliveries | ============================================= */ - - const { - isLoading: fetchDeliveriesIsLoading, - isError: fetchDeliveriesIsError, - error: fetchDeliveriesError, - refetch: fetchDeliveriesRefetch - } = useInfiniteQuery({ - queryKey: [ - 'fetchdeliveries', - appId, - userid, - currentStatus, - startdate, - enddate, - rowsPerPage, - debouncedSearch, - tenantid, - locationid, - riderid - ], - queryFn: fetchDeliveries, - getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined - }); - // (filteredRows is defined below, after countSourceRows / countSourceLoading // are in scope โ€” they're the single source of truth for both the table and // the per-status chip counts.) @@ -740,7 +709,6 @@ const Deliveries = () => { queryFn: () => fetchCountAPI(appId, userid, startdate, enddate, rowsPerPage, debouncedSearch, tenantid, locationid, riderid) }); useEffect(() => { - console.log('countData', countData); if (tabvalue === 0 && countData) { setTotalCount(countData.uncoveredLength); } @@ -755,11 +723,11 @@ const Deliveries = () => { error: ridersListError } = useQuery({ queryKey: ['ridersList', appId], // Unique key for caching & re-fetching + // fetchRidersList now toasts + returns [] internally on failure (was + // throwing after already toasting there too, which showed this same + // error twice) โ€” no onError needed here anymore. queryFn: fetchRidersList, - enabled: Boolean(appId), - onError: (err) => { - OpenToast(err.message, 'error', 2000); - } + enabled: Boolean(appId) }); /* ============================================= || updatedelivery | ============================================= */ @@ -767,26 +735,21 @@ const Deliveries = () => { const updateDeliveryMutation = useMutation({ mutationFn: (orderData) => updateDeliveryAPI(orderData), onSuccess: (res) => { - console.log(res); if (res.success) { opentoast('Updated Successfully', 'success'); setDeliveryamount(''); setNotes(''); setDialogopen(false); - fetchDeliveriesRefetch(); fetchCountRefetch(); countSourceRefetch(); } }, onError: (err) => { - console.log(err); opentoast(err.message, 'success'); } }); - const errorMessage = fetchDeliveriesIsError - ? `Error fetching deliveries: ${fetchDeliveriesError?.message}` - : fetchCountIsError + const errorMessage = fetchCountIsError ? `Error fetching count summary: ${fetchCountError?.message}` : ridersListIsError ? `Error fetching riders: ${ridersListError?.message}` @@ -797,7 +760,6 @@ const Deliveries = () => { : null; if (errorMessage) { - console.log('errorMessage', errorMessage); OpenToast(errorMessage, 'error', 2000); return null; // or return <> if inside a component } @@ -816,7 +778,7 @@ const Deliveries = () => { color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }} - open={fetchCountIsLoading || fetchDeliveriesIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading} + open={fetchCountIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || riderListIsLoading} > {/* */} @@ -1736,8 +1698,8 @@ const Deliveries = () => { {showAction && Action} - {(loading1 || countSourceIsLoading) && } + {(loading1 || countSourceIsLoading) && } {filteredRows.length == 0 && !loading1 && !countSourceLoading && ( @@ -1816,7 +1778,6 @@ const Deliveries = () => { setDeliverylist([...arr]); } } - console.log('list', deliverylist); }} checked={deliverylist.find((res1) => res1.orderheaderid == row.orderheaderid)} /> @@ -2696,7 +2657,6 @@ const Deliveries = () => { setDatestatus(''); } } - console.log(range); }} definedRanges={[ { diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css index e254f05..fcb8486 100644 --- a/src/pages/nearle/dispatch/Dispatch.css +++ b/src/pages/nearle/dispatch/Dispatch.css @@ -17,8 +17,18 @@ .dispatch-container { width: calc(100% + 48px); - height: calc(100vh - 88px); - margin: -24px; + /* MainLayout/index.js's .main-content-area has NO padding-top (only + left/right/bottom at 24px) โ€” Astryx's Layout.tsx renders the TopNav as + a normal-flow flex child ahead of the content area, so content already + starts right below it with no extra offset needed. This page fills the + screen by negative-margining the 24px padding away entirely and sizing + to the viewport height minus the TopNav's own (dynamically measured) + height. An earlier version of this rule compensated for a padding-top + that main-content-area briefly had and no longer does โ€” if that + padding-top ever comes back, this needs to account for it again (see + git history around this comment for that version's math). */ + height: calc(100vh - var(--appshell-header-height, 64px)); + margin: 0 -24px -24px -24px; display: flex; flex-direction: column; background: var(--bg); @@ -10982,7 +10992,7 @@ its stacked children instead of being clipped to a single row height. */ .dispatch-container { height: auto; - min-height: calc(100vh - 88px); + min-height: calc(100vh - var(--appshell-header-height, 64px)); overflow-y: auto; overflow-x: hidden; } diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js index 621baa2..c2bfe08 100644 --- a/src/pages/nearle/dispatch/Dispatch.js +++ b/src/pages/nearle/dispatch/Dispatch.js @@ -1314,7 +1314,16 @@ const Dispatch = ({ const { data: riderInfoData, isFetching: riderInfoFetching, isError: riderInfoIsError, error: riderInfoError } = useQuery({ queryKey: ['riderPeriodicLog', riderInfoUserid], - queryFn: () => getRiderPeriodicLogs(riderInfoUserid), + // riderInfoUserid is actually a userid (it's sourced from ridersAllDay, + // which comes from booking data โ€” assignedmileruserid, confirmed live) + // but /admin/milers/:id/* routes need milerprofileid. liveRiderLocations + // (declared below) carries both, so resolve the real id here rather than + // changing riderInfoUserid itself, which is compared against elsewhere + // in this file for sidebar row highlighting in userid-space. + queryFn: () => { + const match = liveRiderLocations.find((r) => String(r.userid) === String(riderInfoUserid)); + return getRiderPeriodicLogs(match?.milerprofileid ?? riderInfoUserid); + }, enabled: viewMode === 'rider-info' && riderInfoUserid != null, // Auto-refresh the rider snapshot every 15s while the view is open and a // rider is selected. Don't poll while the tab is hidden so we don't burn @@ -1544,8 +1553,14 @@ const Dispatch = ({ const lon = parseFloat(r?.longitude); if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null; return { + // id stays userid-keyed โ€” it's what joins this live-GPS feed against + // `riders`/orders elsewhere in this file, which are themselves keyed + // by a booking's assignedmileruserid (also a userid, confirmed live). + // milerprofileid is kept separately for the one place that actually + // needs it: /admin/milers/:id/* calls (the rider-info panel). id: String(r.userid ?? ''), userid: r.userid, + milerprofileid: r.milerprofileid, username: r.username || `Rider #${r.userid}`, status: String(r.status || '').toLowerCase(), contactno: r.contactno, @@ -1558,12 +1573,15 @@ const Dispatch = ({ .filter(Boolean); }, [ridersLocationLogs]); - // Set of rider ids whose latest GPS log row is `active` (i.e. on the road + // Set of rider ids whose latest position is `active` (i.e. on the road // right now). The "All Active Routes" view (viewMode === 'all') uses this to // show ONLY currently-active riders โ€” their cards, routes, drop markers and // live bike markers โ€” and hide everyone who is offline/idle for the slot. + // GET /admin/milers/summary's availabilitystatus enum is confirmed live as + // Available/Assigned/On_Pickup/Offline โ€” not active/pending, which never + // matched anything and left this set permanently empty. const activeRiderIdSet = useMemo( - () => new Set(liveRiderLocations.filter((r) => r.status === 'active' || r.status === 'pending').map((r) => String(r.id))), + () => new Set(liveRiderLocations.filter((r) => r.status !== 'offline' && r.status !== 'blocked').map((r) => String(r.id))), [liveRiderLocations] ); // Default to the slot containing the current wall-clock time. Use a @@ -1707,6 +1725,12 @@ const Dispatch = ({ const riderDailyData = {}; liveRows.forEach(r => { + // Cancelled/skipped orders never delivered, so they shouldn't earn + // revenue or be charged fuel/slot cost either โ€” matches the same fix + // already applied to ProfitabilitySection.js's calcRiderMetrics. + const status = String(r.orderstatus ?? r.status ?? '').toLowerCase(); + if (SKIPPED_STATUSES.has(status)) return; + // Filter by selectedDate to align with daily slot aggregations const dateStr = r.assigntime ? dayjs(r.assigntime).format('YYYY-MM-DD') @@ -2053,7 +2077,7 @@ const Dispatch = ({ () => isAllActiveView ? liveRiderLocations - .filter((r) => (r.status === 'active' || r.status === 'pending') && activeOrderRiderIdSet.has(String(r.id))) + .filter((r) => r.status !== 'offline' && r.status !== 'blocked' && activeOrderRiderIdSet.has(String(r.id))) .map((r) => [r.lat, r.lon]) : [], [isAllActiveView, liveRiderLocations, activeOrderRiderIdSet] @@ -5129,7 +5153,8 @@ const Dispatch = ({ {/* Live rider GPS markers from GET /admin/milers/summary (see fetchRidersLogs in api.js). Green pin when the rider's status - is `active`, red otherwise, with the rider's username as a + isn't `offline` (Available/Assigned/On_Pickup โ€” confirmed + live enum), red otherwise, with the rider's username as a label. Scoped to riders who actually have orders in the currently selected slot โ€” `riders` is derived from filteredLiveRows so it already reflects the slot filter. A @@ -5139,12 +5164,12 @@ const Dispatch = ({ {liveRiderLocations .filter((r) => isAllActiveView - ? ((r.status === 'active' || r.status === 'pending') && activeOrderRiderIdSet.has(String(r.id))) + ? (r.status !== 'offline' && r.status !== 'blocked' && activeOrderRiderIdSet.has(String(r.id))) : riders.some((rd) => String(rd.id) === String(r.id)) ) .filter((r) => !focusedRider || String(focusedRider.id) === String(r.id)) .map((r) => { - const isActive = r.status === 'active' || r.status === 'pending'; + const isActive = r.status !== 'offline' && r.status !== 'blocked'; const pinColor = isActive ? '#16a34a' : '#dc2626'; // Look up the rider's in-progress order so the popup can show // where they're heading next (drop customer/area + originating diff --git a/src/pages/nearle/dispatch/Preview.js b/src/pages/nearle/dispatch/Preview.js index 6681a90..52d8575 100644 --- a/src/pages/nearle/dispatch/Preview.js +++ b/src/pages/nearle/dispatch/Preview.js @@ -328,7 +328,6 @@ const Preview = () => { 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; @@ -409,11 +408,34 @@ const Preview = () => { }); const createFinalDeliveryMutation = useMutation({ + // finalCreatedeliveries now assigns each order directly on Doormile's + // own booking record (POST /admin/bookings/:id/assign-miler per order, + // resolved internally) instead of posting to jupiter โ€” see api.js for + // why: jupiter's createdeliveries wrote into jupiter's own orphaned + // database, which neither the Orders "pending" list nor the Deliveries + // "dispatched" filter ever read (both come from GET /admin/bookings). mutationFn: finalCreatedeliveries, onSuccess: () => { OpenToast('Delivery Created Successfully', 'success', 2000); setIsLoading(false); - if (rider?.userfcmtoken) notifyRiderMutation.mutate(rider.userfcmtoken); + // stateData.rider (a single rider forwarded via navigate() from the + // Orders page) is never actually populated in the real flow โ€” that + // page's navigate() call doesn't include a `rider` key at all โ€” so + // this was a permanent no-op and no rider ever got notified after + // assignment. Notify every rider actually present in the committed + // list instead. notifyRider expects a milerprofileid (not an FCM + // token โ€” the server looks the device up itself); rider_id/userid + // here is the id this page already treats as canonical throughout + // (see flattenRiders/moveOrderInPreviewData above) since it's the + // only rider identifier the solver echoes back โ€” unconfirmed whether + // that's actually a milerprofileid by the time it reaches here. + const notifiedRiderIds = new Set(); + finaldeliveryList.forEach((order) => { + const riderId = order.rider_id ?? order.userid; + if (riderId == null || notifiedRiderIds.has(String(riderId))) return; + notifiedRiderIds.add(String(riderId)); + notifyRiderMutation.mutate(riderId); + }); navigate('/doormile/deliveries'); }, onError: (error) => { @@ -479,6 +501,18 @@ const Preview = () => { OpenToast('No deliveries to assign', 'error', 3000); return; } + // "Change Rider" and "Reconcile" were two independent buttons with + // nothing linking them โ€” an operator could edit a rider's steps and hit + // Assign Orders without ever pressing Reconcile. Committing stale step + // ordering corrupts route sequences server-side (see this folder's + // CLAUDE.md ยง3, called out as the single biggest production risk here). + // dirtyRiderIds only ever holds riders edited since the last successful + // reconcile of that specific rider, so size > 0 means real unreconciled + // edits are pending. + if (dirtyRiderIds.size > 0) { + OpenToast(`Reconcile ${dirtyRiderIds.size} edited rider(s) before assigning`, 'warning', 4000); + return; + } setIsLoading(true); createFinalDeliveryMutation.mutate({ deliveries: finaldeliveryList }); }; @@ -789,9 +823,18 @@ const Preview = () => { > Back - + 0 ? `Reconcile ${dirtyRiderIds.size} edited rider(s) first` : ''}> + + + + diff --git a/src/pages/nearle/dispatch/ProfitabilitySection.js b/src/pages/nearle/dispatch/ProfitabilitySection.js index 7c1ac26..c1769a8 100644 --- a/src/pages/nearle/dispatch/ProfitabilitySection.js +++ b/src/pages/nearle/dispatch/ProfitabilitySection.js @@ -18,16 +18,20 @@ import './ProfitabilitySection.css'; // Constants // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Exported so reports/profitability.js can reuse the exact same profit math +// instead of hand-duplicating it (a duplication that had already drifted +// into a sync-risk between the two files). + /** Revenue rule: โ‚น30 base for โ‰ค8 km, โ‚น6/km beyond. */ -const BASE_REVENUE = 30; -const BASE_KM_LIMIT = 8; -const EXTRA_RATE_KM = 6; +export const BASE_REVENUE = 30; +export const BASE_KM_LIMIT = 8; +export const EXTRA_RATE_KM = 6; /** Fixed salary cost sliced per slot (โ‚น5000 / 30 days / 1 slot). */ -const FIXED_COST_PER_SLOT = 500 / 3; +export const FIXED_COST_PER_SLOT = 500 / 3; /** Variable fuel / wear cost per km. */ -const VARIABLE_RATE_KM = 2.5; +export const VARIABLE_RATE_KM = 2.5; /** Status display config keyed by normalised status string. */ const STATUS_MAP = { @@ -54,7 +58,7 @@ function orderRevenue(order) { return km <= BASE_KM_LIMIT ? BASE_REVENUE : BASE_REVENUE + (km - BASE_KM_LIMIT) * EXTRA_RATE_KM; } -const BATCHES_DEFAULT = [ +export const BATCHES_DEFAULT = [ { id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 }, { id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12.5 }, { id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 } @@ -77,7 +81,16 @@ const getRowBatch = (r, batches = BATCHES_DEFAULT) => { return getBatchForHour(d.hour() + d.minute() / 60, batches); }; +// Orders in these statuses never delivered, so they earn no revenue and +// shouldn't be charged fuel/slot cost either โ€” including them here made +// every rider's profit/margin KPI count cancelled and skipped stops as if +// they'd been completed. +const EXCLUDED_FROM_PROFIT_STATUSES = new Set(['cancelled', 'skipped']); + function calcRiderMetrics(rider, selectedDate, batches) { + // `orders` stays the full date-filtered set โ€” the breakdown table and + // order-count KPI intentionally still show cancelled/skipped stops for + // context. Only the money/km math below excludes them. const orders = (rider.orders ?? []).filter((o) => { if (!selectedDate) return true; const dateStr = o.assigntime @@ -87,11 +100,15 @@ function calcRiderMetrics(rider, selectedDate, batches) { : 'unknown'; return dateStr === selectedDate; }); + const billableOrders = orders.filter((o) => { + const status = String(o.orderstatus ?? o.status ?? '').toLowerCase(); + return !EXCLUDED_FROM_PROFIT_STATUSES.has(status); + }); let revenue = 0; let kms = 0; const slotsByDate = {}; - for (const o of orders) { + for (const o of billableOrders) { revenue += orderRevenue(o); kms += parseFloat(o.riderkms || 0); diff --git a/src/pages/nearle/hubs/hubs.js b/src/pages/nearle/hubs/hubs.js index e68194d..fb41168 100644 --- a/src/pages/nearle/hubs/hubs.js +++ b/src/pages/nearle/hubs/hubs.js @@ -65,7 +65,10 @@ const emptyForm = { latitude: '', longitude: '', pincode: '', - status: 'active' + // Confirmed live: POST /admin/hubs rejects lowercase status ("active" + // fails, "Active" works) โ€” Inactive was never confirmed to work even on + // create, so this only offers the confirmed-working value here. + status: 'Active' }; const Hubs = () => { @@ -138,7 +141,7 @@ const Hubs = () => { latitude: row.latitude ?? '', longitude: row.longitude ?? '', pincode: row.pincode || '', - status: row.status || 'active' + status: row.status || 'Active' }); setDialogOpen(true); }; @@ -353,8 +356,8 @@ const Hubs = () => { Status diff --git a/src/pages/nearle/login.js b/src/pages/nearle/login.js index 080687b..754baf2 100644 --- a/src/pages/nearle/login.js +++ b/src/pages/nearle/login.js @@ -169,7 +169,6 @@ const Login = () => { label="E-mail Address" type="email" size="lg" - isRequired value={email} onChange={(value) => setEmail(value.toLocaleLowerCase())} /> @@ -177,7 +176,6 @@ const Login = () => { label="Password" type="password" size="lg" - isRequired value={password} onChange={(value) => setPassword(value)} /> @@ -189,13 +187,13 @@ const Login = () => { {/* footer */} - + © All rights reserved - + Terms and Conditions - + Privacy Policy diff --git a/src/pages/nearle/orders/OrdersPreview.js b/src/pages/nearle/orders/OrdersPreview.js index eb58df9..0df13f9 100644 --- a/src/pages/nearle/orders/OrdersPreview.js +++ b/src/pages/nearle/orders/OrdersPreview.js @@ -187,7 +187,6 @@ const OrdersPreview = () => { const isMobile = useMediaQuery(theme.breakpoints.down('md')); const navigate = useNavigate(); const location = useLocation(); - console.log('location.state', location.state); const [rider, setRider] = useState(null); const [payment, setPayment] = useState(null); const [finaldeliveryList, setFinalDeliveryList] = useState([]); @@ -200,14 +199,6 @@ const OrdersPreview = () => { const aiMode = location.state?.aiMode; const reassignOrders = location.state?.reassignOrders; - useEffect(() => { - console.log('aiMode', aiMode); - console.log('riderToken', riderToken); - console.log('zoneData', zoneData); - console.log('metaData', metaData); - console.log('reassignOrders', reassignOrders); - }, []); - useEffect(() => { if (!deliverylist?.length) return; const updateDeliveryAmtList = deliverylist.map((list) => { @@ -228,7 +219,6 @@ const OrdersPreview = () => { }); setFinalDeliveryList(updateDeliveryAmtList); - console.log('finaldeliveryList', updateDeliveryAmtList); }, [deliverylist]); // ==============================|| fetchPaymentType ||============================== // @@ -275,16 +265,27 @@ const OrdersPreview = () => { const createNormalDeliveryMutation = useMutation({ mutationFn: finalCreatedeliveries, // for optimised delivery create - onSuccess: (data, variables) => { - console.log('data', data); - console.log('varialbles', variables); - notifyRiderMutation.mutate(rider?.userfcmtoken || riderToken); // Call notifyRider after success + onSuccess: (data) => { + // notifyRider expects a milerprofileid, not an FCM token โ€” the server + // looks the device up itself (see dispatch/Preview.js's identical + // fix). rider?.userfcmtoken/riderToken here were always the wrong + // type, so every notify call from this page 404'd silently. Notify + // every rider actually present across the committed deliveries + // instead, keyed by the same `userid` field this page already uses + // for rider identity (see the Change Rider handlers below). + const notifiedRiderIds = new Set(); + (finaldeliveryList || []).forEach((order) => { + const riderId = order.userid; + if (riderId == null || notifiedRiderIds.has(String(riderId))) return; + notifiedRiderIds.add(String(riderId)); + notifyRiderMutation.mutate(riderId); + }); if (data.status == 'accepted') { OpenToast('Delivery Created Successfully', 'success', 2000); } setTimeout(() => { setIsLoading(false); - navigate('/nearle/orders'); + navigate('/doormile/orders'); }, 2000); }, onError: (error) => { @@ -306,7 +307,7 @@ const OrdersPreview = () => { navigate('/nearle/orders')} + onClick={() => navigate('/doormile/orders')} sx={{ backgroundColor: 'action.hover', color: 'text.primary', @@ -696,7 +697,6 @@ const OrdersPreview = () => { return; } if (newValue) { - console.log('Selected:', newValue); setPayment(newValue); const newList = finaldeliveryList?.map((list) => ({ ...list, @@ -719,7 +719,6 @@ const OrdersPreview = () => { } if (newValue) { setRider(newValue); - console.log('Selected:', newValue); const newList = finaldeliveryList?.map((list) => ({ ...list, userid: newValue.userid, @@ -747,7 +746,7 @@ const OrdersPreview = () => { color="secondary" startIcon={} onClick={() => { - navigate('/nearle/orders'); + navigate('/doormile/orders'); }} > Back diff --git a/src/pages/nearle/orders/createorder1.js b/src/pages/nearle/orders/createorder1.js index bf39c89..2b3ea1e 100644 --- a/src/pages/nearle/orders/createorder1.js +++ b/src/pages/nearle/orders/createorder1.js @@ -45,7 +45,14 @@ import { useNavigate } from 'react-router'; import { MdLocationCity } from 'react-icons/md'; import { TbMapPinCode } from 'react-icons/tb'; import { FaLocationDot } from 'react-icons/fa6'; -import { getAdminPricing, getAdminTenant, getTenantCustomers, getTenantLocations, createExpressBooking } from 'pages/api/doormileApi'; +import { + getAdminPricing, + getAdminTenant, + getAdminTenants, + getAdminCustomers, + getTenantLocations, + createExpressBooking +} from 'pages/api/doormileApi'; import { useTheme } from '@mui/material/styles'; import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete'; import Loader from 'components/Loader'; @@ -175,6 +182,32 @@ const SoftPaper = (props) => ( /> ); +// Some saved locations come back from GET /admin/tenants/:id/locations with +// a blank locationname (live-confirmed, e.g. tenant 1's only location) โ€” fall +// back to the address rather than rendering a bare icon with no text. +const locationDisplayName = (loc) => (loc?.locationname && loc.locationname.trim()) || loc?.address || 'Unnamed location'; + +// Tenants with many locations in the same city (DailyGrubs has 23, all in +// Coimbatore) made the old "(city)" suffix useless for telling them apart โ€” +// sort primary-first then alphabetically so the picker is at least ordered +// the same way every time; the full address is shown per-option instead +// (see renderOption on the Business Location Autocomplete below). +const sortLocations = (locations) => + [...(locations || [])].sort((a, b) => { + if (!!a.isprimary !== !!b.isprimary) return a.isprimary ? -1 : 1; + return locationDisplayName(a).localeCompare(locationDisplayName(b)); + }); + +// POST /admin/expressbooking's parcels[].itemcategory is a free-text string +// (see express-console-api.md's sample body) โ€” the old jupiter backend had a +// real /utils/getsubcategories lookup table behind this dropdown, but that +// endpoint has no equivalent on the new API (confirmed โ€” see +// src/pages/api/CLAUDE.md's list of retired-with-no-replacement resources). +// This is a fixed, curated list rather than a live lookup so the dropdown +// actually has options; `freeSolo` lets an operator type a category that +// isn't on the list instead of being blocked. +const PARCEL_CATEGORIES = ['Food', 'Groceries', 'Documents', 'Electronics', 'Clothing & Apparel', 'Medicines', 'Furniture', 'Others']; + const Createorder1 = () => { // ================================================= || GoogleMaps (Drawer) || ================================================= const [inputValue, setInputValue] = React.useState(''); @@ -205,6 +238,11 @@ const Createorder1 = () => { const autocompleteService = useRef(null); const [tenanatLocoId, setTenanatLocoId] = useState(localStorage.getItem('locationid')); const [isLocation, setIsLocation] = useState(false); + // Drives what shows in place of the Business Location picker. A toast alone + // wasn't enough โ€” it's easy to miss, and an empty Autocomplete with no + // toast visible reads as "broken" with zero explanation. This makes the + // reason a persistent, visible message instead. + const [locationsStatus, setLocationsStatus] = useState('loading'); const textFieldRef1 = useRef(null); const textFieldRef2 = useRef(null); const [collectionamt, setCollectionamt] = useState(0); @@ -269,7 +307,6 @@ const Createorder1 = () => { const [tenantinfo, setTenantinfo] = useState({}); const [searchword, setSearchword] = useState(''); const [clientdetailarr, setClientdetailarr] = useState([]); - const [clientdetailbusinessarr, setClientdetailbusinessarr] = useState([]); const [admintoken, setAdmintoken] = useState(); const [tenantlocationlist, setTenantlocationlist] = useState([]); const [tenant, setTenant] = useState({}); @@ -289,11 +326,20 @@ const Createorder1 = () => { const [pricePerKm, setPricePerKm] = useState(0); const [minKm, setMinKm] = useState(0); const [totalCharge, setTotalCharge] = useState(0); - const [subCat, setSubCat] = useState([]); - const [subCatName, setSubCatName] = useState('Select '); - const [subCatId, setSubCatId] = useState(); + const [category, setCategory] = useState(null); const [weight, setWeight] = useState(''); - const tid = localStorage.getItem('tenantid'); + // Per express-console-api.md's "Tenant scoping" section: token tenantid + // 0/null means Doormile's own staff, who can see/act on every tenant โ€” + // NOT an error state. A client (tenant) login carries a real tenantid and + // never needs this picker at all. loggedInTenantId is what's actually on + // the account; selectedTenantId is what the page operates as right now + // (auto-filled for a client login, chosen from the picker for staff). + const loggedInTenantId = localStorage.getItem('tenantid'); + const isStaffLogin = !loggedInTenantId || loggedInTenantId === '0'; + const [selectedTenantId, setSelectedTenantId] = useState(isStaffLogin ? '' : loggedInTenantId); + const [staffTenantList, setStaffTenantList] = useState([]); + const [staffTenantListLoading, setStaffTenantListLoading] = useState(false); + const tid = selectedTenantId; const [selectedCatChip, setSelectedCatChip] = useState(null); const [isCustomerOpen, setIsCustomerOpen] = useState(false); const [searchCustList, setSearchCustList] = useState(''); @@ -317,10 +363,6 @@ const Createorder1 = () => { const [numErr2, setNumErr2] = useState(false); const [isSms, setIsSms] = useState(0); - useEffect(() => { - console.log(isSms); - }, [isSms]); - const handleChipClick = (chipLabel) => { setSelectedCatChip(chipLabel); }; @@ -338,23 +380,24 @@ const Createorder1 = () => { const fetchTenantPricing = async () => { try { const pricing = (await getAdminPricing()) || []; - const match = pricing.find((p) => p.tenantid === tid); + // tid comes from localStorage (always a string) but pricing.tenantid is + // numeric โ€” a strict === here never matched, so basePrice/pricePerKm/ + // minKm stayed at their 0 defaults and every order priced at โ‚น0. + const match = pricing.find((p) => String(p.tenantid) === String(tid)); if (match) { setBasePrice(match.baseprice); setPricePerKm(match.priceperkm); setMinKm(match.basedistance); } } catch (error) { - console.log('fetchTenantPricing error', error); + // swallow โ€” pricing defaults to 0 and stays there, handled upstream } }; useEffect(() => { - fetchTenantPricing(); - }, []); + if (tid) fetchTenantPricing(); + }, [tid]); useEffect(() => { - console.log('startPoint', startPoint); - console.log('endPoint', endPoint); if (startPoint.latitude != 0 && startPoint.longitude != 0 && endPoint.latitude != 0 && endPoint.longitude != 0) { // getDistance(); calculateDistance(startPoint, endPoint); @@ -363,10 +406,8 @@ const Createorder1 = () => { const getDistance = () => { const dist = geolib.getPreciseDistance(startPoint, endPoint, 1); - console.log(`Distance: ${dist} meters`); const distanceInKm = (dist / 1000).toFixed(2); const roundedDistance = Math.round(distanceInKm); - console.log('roundedDistance', roundedDistance); setDistance(roundedDistance.toFixed(2)); if (roundedDistance < minKm) { setTotalCharge(basePrice); @@ -384,11 +425,9 @@ const Createorder1 = () => { const calculateDistance = async (pickup, drop) => { try { const roundedDistance = await calculateDrivingDistance(pickup, drop); - console.log('calculated distance in km:', roundedDistance); setDistance(roundedDistance); const total = calculateTotalCharge(roundedDistance, basePrice, pricePerKm, minKm); - console.log('total charge:', total); setTotalCharge(total); setShowDistance(true); @@ -396,11 +435,6 @@ const Createorder1 = () => { setShowDistance(true); setOpen4(true); } - - // โฑ๏ธ Approximate duration - const avgSpeed = 40; // km/h (adjust if needed) - const duration = Math.round((roundedDistance / avgSpeed) * 60); // minutes - console.log(`Distance: ${roundedDistance}, Duration: ${duration}`); } catch (error) { console.error('Error calculating distance:', error); } @@ -442,7 +476,6 @@ const Createorder1 = () => { val.contactno.toLowerCase().includes(searchword.toLowerCase()) ); }); - console.log(arr); setClientdetailarr([...arr]); } else { setClientdetailarr([...clientdetail]); @@ -458,22 +491,18 @@ const Createorder1 = () => { setTenant(info); setTenantid(info.tenantid); fetchAppAdminTokens(); - setSubCatId(info.subcategoryid); } } catch (err) { - console.log(err); + // doormileAxios's response interceptor rejects with error.response.data + // (not the raw axios error), so the message lives at err.message here. + opentoast(err?.message || 'Could not load tenant info', 'error', 3000); } finally { setLoading(false); } }; useEffect(() => { - fetchtenantinfo(); - }, []); - // No item-subcategory lookup endpoint in the new API. - const getsubcategories = async () => setSubCat([]); - useEffect(() => { - getsubcategories(); - }, []); + if (tid) fetchtenantinfo(); + }, [tid]); // ==================================================== || fetchTiming || ==================================================== // No per-zone open/close-hours or geofence-radius endpoint in the new API @@ -523,7 +552,6 @@ const Createorder1 = () => { anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: time ? time : 2000 }); - console.log(alertmessage); }; function closeAddressModal() { @@ -538,29 +566,25 @@ const Createorder1 = () => { const createsubmitobj2 = async () => { // POST /admin/expressbooking's schema is a single flat booking object - // (tenantid, pickuplocationid, customer_phone/name, delivery* fields, + // (tenantid, pickup* fields, customer_phone/name, delivery* fields, // service_option, finalprice, notes, parcels[]) โ€” nothing like the old // pickup/drop/orders triple. The drop contact becomes the booking's - // customer (new API models the delivery recipient as "the customer"); - // the pickup side maps to pickuplocationid when a saved location was - // picked (tenanatLocoId/isLocation, set by the "Business Location" - // Autocomplete below โ€” pickCust.deliverylocationid was always '' and - // never actually got wired to the picker, so this shortcut never fired), - // otherwise raw pickup* fields following the doc's own delivery*/pickup* - // naming symmetry (pickupaddress/pickuppincode/pickupcity/pickuplatitude/ - // pickuplongitude โ€” NOT shown in the doc's example, so unverified against - // the live API). + // customer (new API models the delivery recipient as "the customer"). + // + // Always send raw pickupaddress/pickuppincode/pickupcity/pickuplatitude/ + // pickuplongitude, even when a saved location was picked (the Business + // Location Autocomplete + gettenantlocations()'s single-location + // auto-select both copy the chosen location's fields into pickCust) โ€” + // live-confirmed the pickuplocationid shortcut currently 500s on + // POST /admin/expressbooking (backend bug, reported; not something a + // frontend fix can work around other than avoiding the field). const arr = { - tenantid: tenant.tenantid, - ...(isLocation && tenanatLocoId - ? { pickuplocationid: tenanatLocoId } - : { - pickupaddress: pickCust.address || '', - pickuppincode: pickCust.postcode || '', - pickupcity: pickCust.city || '', - pickuplatitude: Number(pickCust.latitude) || 0, - pickuplongitude: Number(pickCust.longitude) || 0 - }), + tenantid: Number(tid) || tenant.tenantid, + pickupaddress: pickCust.address || '', + pickuppincode: pickCust.postcode || '', + pickupcity: pickCust.city || '', + pickuplatitude: Number(pickCust.latitude) || 0, + pickuplongitude: Number(pickCust.longitude) || 0, customer_phone: dropCust.contactno || '', customer_name: dropCust.firstname || '', deliveryaddress: dropCust.address || '', @@ -573,14 +597,15 @@ const Createorder1 = () => { notes: otherinstructions || '', parcels: [ { - itemcategory: subCat.find((c) => c.subcategoryid === subCatId)?.categoryname || 'General', + itemcategory: category || 'General', itemdescription: 'Order', declaredvalue: +totalCharge.toFixed(2) || 0 } ] }; - console.log('createsubmitobj2', arr); - if (!pickCust.firstname) { + if (!arr.tenantid) { + opentoast('Pick a tenant before creating an order', 'warning', 2000); + } else if (!pickCust.firstname) { opentoast('Enter Pickup Contact Name ', 'warning', 2000); } else if (!pickCust.contactno) { opentoast('Enter Pickup Contact Number ', 'warning', 2000); @@ -610,8 +635,6 @@ const Createorder1 = () => { opentoast('Enter Drop Landmark ', 'warning', 2000); } else if (!selectedtime) { opentoast('Choose deliverytime ', 'warning', 2000); - } else if (!setSubCatId) { - opentoast('Choose SubCategory ', 'warning', 2000); } else { setLoading(true); try { @@ -622,82 +645,65 @@ const Createorder1 = () => { anchorOrigin: { vertical: 'top', horizontal: 'right' }, autoHideDuration: 1000 }); - navigate('/nearle/orders'); + navigate('/doormile/orders'); } else { opentoast(createRes.message || 'Something went wrong, Cannot create order', 'warning'); } } catch (error) { - console.log('createResErr', error); // Previously silent โ€” a thrown error here (network failure, CityGate // rejection, validation error) left the operator with no feedback at // all and a permanently stuck loading spinner (setLoading(false) was - // only reachable on the success path). - opentoast(error.response?.data?.message || error.message || 'Cannot create order', 'error', 3000); + // only reachable on the success path). doormileAxios's response + // interceptor already unwraps to error.response.data before + // rejecting, so the message lives at error.message, not + // error.response.data.message. + opentoast(error?.message || 'Cannot create order', 'error', 3000); } finally { setLoading(false); } } }; - useEffect(() => { - console.log('shiftarr'); - console.log(shiftarr); - }, [shiftarr]); - const clicked1 = (e) => { setShift(e); }; const dialogopen = (i, result) => { - console.log(i, result); setOpen({ shiftsno: result.sno, sno: i.sno }); }; const dialogclose = () => { setOpen(''); }; // ========================================================= || clientdetails || ========================================================= + // GET /admin/customers (B2C directory), not /admin/tenantcustomers โ€” that + // resource is a manually-added contact book with nothing in it unless + // someone explicitly creates an entry. This one is confirmed to auto-grow: + // POST /admin/expressbooking's customer_phone/customer_name "creates a + // Guest customer if unknown" (see express-console-api.md), so every order + // placed lands a reusable contact here โ€” matching the old jupiter flow + // where placing an order (customerid: 0) auto-saved the customer too. + // Field shape is { appcustomerid, createdat, email, name, phone, + // totalbookings } โ€” a single `name`, not firstname/lastname, and still no + // address at all (see the Saved Contacts dialog below). const clientdetails = async () => { setLoading2(true); try { - const customers = (await getTenantCustomers()) || []; + const customers = (await getAdminCustomers()) || []; const filtered = searchCustList - ? customers.filter( - (val) => - val.firstname?.toLowerCase().includes(searchCustList.toLowerCase()) || - val.phone?.includes(searchCustList) || - val.contactno?.includes(searchCustList) - ) + ? customers.filter((val) => val.name?.toLowerCase().includes(searchCustList.toLowerCase()) || val.phone?.includes(searchCustList)) : customers; setClientdetail(filtered); setCustomerlist(filtered); - setClientdetailarr(filtered.map((val) => ({ label: `${val.firstname} | ${val.phone || val.contactno}`, ...val }))); + setClientdetailarr(filtered.map((val) => ({ label: `${val.name} | ${val.phone}`, ...val }))); } catch (err) { - console.log(err); opentoast('server error', 'warning'); } finally { setLoading2(false); } }; useEffect(() => { - console.log('clientdetails, by search'); clientdetails(); - // clientdetailsbusiness(); }, [searchCustList.length > 3, searchCustList == '']); - // ========================================================= || clientdetailsbusiness || ========================================================= - const clientdetailsbusiness = async () => { - setLoading2(true); - try { - const customers = (await getTenantCustomers()) || []; - setClientdetail(customers); - setClientdetailbusinessarr(customers.map((val) => ({ label: `${val.firstname} | ${val.phone || val.contactno}`, ...val }))); - } catch (err) { - console.log(err); - opentoast('server error', 'warning'); - } finally { - setLoading2(false); - } - }; - // ================================================== || sendnotifications || ================================================== // No bulk-notify endpoint in the new API โ€” never actually called (dead in // the original code too, only referenced from a commented-out call site). @@ -827,20 +833,69 @@ const Createorder1 = () => { // ============================================= || gettenantlocations (branches) || ============================================= const gettenantlocations = async () => { + if (!tid) { + // Staff login (express-console-api.md: token tenantid 0/null = Doormile + // staff, who can act on any tenant) with no tenant picked yet โ€” the + // picker UI covers this, not an error. A genuinely tenant-less, + // non-staff account can't get here at all (isStaffLogin would be true). + setLocationsStatus('no-tenant'); + return; + } + setLocationsStatus('loading'); try { const locations = (await getTenantLocations(tid)) || []; setTenantlocations(locations); - if (locations.length == 1) { + if (locations.length === 1) { + setLocationsStatus('ready'); setIsLocation(true); - setTenanatLocoId(locations[0].locationid); + setTenanatLocoId(locations[0].tenantlocationid); + const loc = locations[0]; + setPickCust((prev) => ({ + ...prev, + address: loc.address || '', + suburb: loc.locationname || loc.city || '', + city: loc.city || '', + postcode: loc.pincode || '', + landmark: loc.address || '', + latitude: loc.latitude, + longitude: loc.longitude + })); + } else if (locations.length === 0) { + // No saved locations for this tenant โ€” every pickup field below is a + // plain manual TextField anyway, so let the operator fill it in by + // hand instead of leaving the form permanently disabled. + setLocationsStatus('empty'); + setIsLocation(true); + opentoast('No saved business locations โ€” enter pickup details manually below', 'info', 3000); + } else { + setLocationsStatus('ready'); } } catch (err) { - console.log('gettenantlocations', err); + // doormileAxios's response interceptor rejects with error.response.data + // (not the raw axios error) โ€” so the message is err.message here, not + // err.response.data.message (that was reading through a layer that no + // longer exists at this point and always fell back to the generic text). setTenantlocations([]); + setLocationsStatus('error'); + opentoast(err?.message || 'Could not load business locations', 'error', 4000); } }; useEffect(() => { gettenantlocations(); + }, [tid]); + + // ============================================= || fetchStaffTenantList (Doormile staff picker) || ============================================= + // Only staff logins (loggedInTenantId blank/0) need this โ€” a client login's + // tenant is already fixed by their token and this never fetches for them. + useEffect(() => { + if (!isStaffLogin) return; + setStaffTenantListLoading(true); + getAdminTenants() + .then((tenants) => setStaffTenantList((tenants || []).filter((t) => t.status === 'Active'))) + .catch((err) => { + opentoast(err?.message || 'Could not load tenant list', 'error', 4000); + }) + .finally(() => setStaffTenantListLoading(false)); }, []); return ( @@ -853,7 +908,7 @@ const Createorder1 = () => { elevation={0} sx={{ position: 'sticky', - top: '60px', + top: 'var(--appshell-header-height, 64px)', zIndex: 1000, mb: 1.5, p: { xs: 1.5, sm: 2, md: 2.5 }, @@ -911,7 +966,124 @@ const Createorder1 = () => { - {tenantLocations.length === 1 ? ( + {locationsStatus === 'loading' ? ( + + Loading business locationsโ€ฆ + + ) : locationsStatus === 'no-tenant' && isStaffLogin ? ( + // Staff login (express-console-api.md: token tenantid 0/null + // = "Doormile's own staff... sees everything, all tenants") + // โ€” pick which tenant to create this order for. Everything + // downstream (pricing, saved locations, the booking's own + // tenantid) is keyed off `tid`, which equals selectedTenantId. + option?.tenantname || ''} + isOptionEqualToValue={(option, value) => option?.tenantid === value?.tenantid} + PaperComponent={SoftPaper} + sx={{ width: { xs: '100%', sm: 300 } }} + size="small" + onChange={(event, value) => setSelectedTenantId(value ? String(value.tenantid) : '')} + 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 } + } + }} + /> + )} + /> + ) : locationsStatus === 'no-tenant' ? ( + + This login has no tenant assigned โ€” sign in as a tenant user + + ) : locationsStatus === 'error' ? ( + + Could not load business locations โ€” tap to retry + + ) : locationsStatus === 'empty' ? ( + + No saved locations โ€” enter pickup details manually below + + ) : tenantLocations.length === 1 ? ( { fontSize: 13 }} > - {tenantLocations[0].locationname} + {locationDisplayName(tenantLocations[0])} ) : ( (option && option.locationname ? `${option.locationname} (${option.suburb || ''})` : '')} - isOptionEqualToValue={(option, value) => option?.locationid === value?.locationid} + options={sortLocations(tenantLocations)} + getOptionLabel={(option) => locationDisplayName(option)} + isOptionEqualToValue={(option, value) => option?.tenantlocationid === value?.tenantlocationid} + renderOption={(props, option) => ( + + + + + {locationDisplayName(option)} + + {option.isprimary && ( + + PRIMARY + + )} + + + {option.address || option.city || ''} + + + + )} PaperComponent={SoftPaper} - sx={{ width: { xs: '100%', sm: 300 } }} + sx={{ width: { xs: '100%', sm: 320 } }} size="small" renderInput={(params) => ( { )} onChange={(event, value, reason) => { if (value) { - setTenanatLocoId(value.locationid); + setTenanatLocoId(value.tenantlocationid); setIsLocation(true); + // Also fill pickCust's raw address fields from the + // picked location โ€” POST /admin/expressbooking's + // pickuplocationid currently 500s server-side (live- + // confirmed), so createsubmitobj2() always sends raw + // pickup* fields instead; this keeps that path working + // when a saved location is chosen here. + setPickCust((prev) => ({ + ...prev, + address: value.address || '', + suburb: value.locationname || value.city || '', + city: value.city || '', + postcode: value.pincode || '', + landmark: value.address || '', + latitude: value.latitude, + longitude: value.longitude + })); } if (reason === 'clear') { setIsLocation(false); @@ -1024,7 +1242,7 @@ const Createorder1 = () => { } }} > - Saved Locations + Saved Contacts } /> @@ -1097,7 +1315,6 @@ const Createorder1 = () => {
{ } }} > - Saved Locations + Saved Contacts } /> @@ -1418,7 +1635,6 @@ const Createorder1 = () => { opentoast('Select Another Customer', 'error'); setSearchCustList(''); } else { - console.log('DropClient', val); setDropCust(val); setEndPoint({ latitude: val.latitude, longitude: val.longitude }); setAddId2(1); @@ -1513,7 +1729,6 @@ const Createorder1 = () => {
{ format="DD-MM-YYYY" onChange={(e) => { let dateres11 = dayjs().diff(dayjs(`${dayjs(e).format('YYYY-MM-DD')}`), 'd'); - console.log('dateres11'); - console.log(dateres11); setSelectedtime(''); if (dateres11 <= 0) { - console.log('startdate', e); setStartdate(e); setEnddate(e); @@ -1881,20 +2093,16 @@ const Createorder1 = () => { Category `${option.subcategoryname}` || ''} + options={PARCEL_CATEGORIES} + value={category} sx={{ my: 2, zIndex: '100' }} fullWidth - renderInput={(params) => ( - - )} - onChange={(event, value, reason) => { - if (value) { - console.log(value); - setSubCatName(value.subcategoryname); - setSubCatId(value.subcategoryid); - } + renderInput={(params) => } + onChange={(event, value) => setCategory(value || null)} + onInputChange={(event, value, reason) => { + if (reason === 'input') setCategory(value || null); }} /> @@ -2126,10 +2334,10 @@ const Createorder1 = () => { - Saved Locations + Saved Contacts - Pick a saved customer for {pickordrop === 1 ? 'Pickup' : 'Drop'} + Pick a saved contact's name & phone for {pickordrop === 1 ? 'Pickup' : 'Drop'} โ€” address isn't stored, search it below @@ -2179,32 +2387,38 @@ const Createorder1 = () => { - No saved locations + No saved contacts - - {searchCustList ? 'Try a different keyword.' : 'Save a customer from the order form to reuse it later.'} + + {searchCustList ? 'Try a different keyword.' : 'Place an order with a new contact โ€” it will show up here next time.'} ) : ( {customerlist.map((address, index) => { - const isUsed = pickCust.customerid === address.customerid; + // GET /admin/customers (B2C directory) โ€” { appcustomerid, + // createdat, email, name, phone, totalbookings }. No + // address/coordinates at all (that was a jupiter-only field + // on the old customer record). Picking a saved contact here + // only pre-fills the name & phone; the operator still has + // to search the address below like a brand-new contact. + const contactPhone = address.phone; + const isUsed = !!contactPhone && pickCust.phone === contactPhone; return ( { if (isUsed) return; setIsCustomerOpen(false); + const contact = { firstname: address.name, contactno: contactPhone, phone: contactPhone }; if (pickordrop === 1) { setAddId1(1); - setStartPoint({ latitude: address.latitude, longitude: address.longitude }); - setPickCust(address); - setPickNum(address.contactno); + setPickCust((prev) => ({ ...prev, ...contact })); + setPickNum(contactPhone); } else { setAddId2(1); - setEndPoint({ latitude: address.latitude, longitude: address.longitude }); - setDropCust(address); - setdropNum(address.contactno); + setDropCust((prev) => ({ ...prev, ...contact })); + setdropNum(contactPhone); } }} sx={{ @@ -2229,14 +2443,14 @@ const Createorder1 = () => { }} > - {String(address.firstname || '?') + {String(address.name || '?') .charAt(0) .toUpperCase()} - {address.firstname} + {address.name || 'Unnamed'} { fontWeight: 800 }} > - {address.contactno} + {contactPhone || 'โ€”'} {isUsed && ( { )} - - {address.address} + + {address.email || 'No address on file โ€” search it below'} diff --git a/src/pages/nearle/orders/details.js b/src/pages/nearle/orders/details.js deleted file mode 100644 index 97a4732..0000000 --- a/src/pages/nearle/orders/details.js +++ /dev/null @@ -1,2344 +0,0 @@ -/* eslint-disable no-unused-vars */ -import { - useEffect, useState, Fragment - // useReducer -} from 'react'; -import BorderColorIcon from '@mui/icons-material/BorderColor'; -import { - // Navigate, - // useSearchParams, - useLocation, - useNavigate -} from 'react-router-dom'; -import axios from 'axios'; -import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import { - // UserOutlined, - EnvironmentOutlined, - EditTwoTone - // DeleteTwoTone -} from '@ant-design/icons'; -// import WomanIcon from '@mui/icons-material/Woman'; -// import { Link } from 'react-router-dom'; -// import SoupKitchenIcon from '@mui/icons-material/SoupKitchen'; -import DirectionsCarIcon from '@mui/icons-material/DirectionsCar'; -import SendIcon from '@mui/icons-material/Send'; -import { KeyboardArrowUp, KeyboardArrowDown } from '@mui/icons-material' - -// import { PopupTransition } from 'components/@extended/Transitions'; -// import { useDispatch } from 'react-redux'; -// import { openSnackbar } from 'store/reducers/snackbar'; -// assets -import { DeleteFilled, NotificationOutlined } from '@ant-design/icons'; -var utc = require('dayjs/plugin/utc') -// import { groupBy } from "core-js/actual/array/group-by"; -// import "lodash.chunk"; -// var chunk = require('lodash.chunk'); -import { - Grid, - Typography, - Stack, - Button as Button1, - // Box, - FormControl, - Table, - TableHead, - TableRow, - TableCell, - TableContainer, - TableBody, - Chip, - Divider, - CardActions, - IconButton, - Avatar, - Dialog, - DialogTitle, - DialogContent, - Checkbox, - Tooltip, - Button, - Tab, - Tabs, - Skeleton, - // DialogContent, - DialogActions, - TextField, - CircularProgress, - Collapse, - Alert, - AlertTitle, - Link - // Card, - // CardHeader -} from '@mui/material'; - -import { PopupTransition } from 'components/@extended/Transitions'; - -// import ControlPointIcon from '@mui/icons-material/ControlPoint'; -import CancelOutlinedIcon from '@mui/icons-material/CancelOutlined'; -import MainCard from 'components/MainCard'; -import Loader from 'components/Loader'; -// import AlertCustomerDelete from 'sections/apps/customer/AlertCustomerDelete'; -import dayjs from 'dayjs'; -dayjs.extend(utc) -// import { Link as RouterLink } from 'react-router-dom'; -// import PlayCircleFilled from '@mui/icons-material/PlayCircleFilled'; -// import SmileFilled from '@mui/icons-material/Mood'; -// import HeartFilled from '@mui/icons-material/Favorite'; - -import { useTheme } from '@mui/material/styles'; -import { - CloseOutlined, - WarningOutlined, - WarningFilled - // PlusOutlined, - // EyeTwoTone, - // EditTwoTone, - // 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 = '#C01227'; -const BRAND_LIGHT = '#D25463'; - -// 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(); - const [orderarr, setOrderarr] = useState([]); - const [loading, setLoading] = useState(true); - const location = useLocation(); - const [orderid, setOrderid] = useState(''); - const [eventlocation, setEventlocation] = useState(''); - const [orderdate, setOrderdate] = useState(''); - const [eventname, setEventname] = useState(''); - const [open, setOpen] = useState(false); - const [clientname, setClientname] = useState(''); - // const [duedate, setDuedate] = useState(''); - const [tenantaddress, setTenantaddress] = useState(''); - const [dialogopen, setDialogopen] = useState(false); - const [orderstatus, setOrderstatus] = useState(''); - const [currentrole, setCurrentrole] = useState(''); - const [taxamount, setTaxamount] = useState(''); - const [subtotal, setSubtotal] = useState(''); - const [grandtotal, setGrandtotal] = useState(''); - const [venuetype, setVenuetype] = useState(''); - const [orderaddons, setOrderaddons] = useState([]); - const [otherinstructions, setOtherinstructions] = useState(''); - const [cancelleddate, setCancelleddate] = useState(''); - const [stafflist, setStafflist] = useState([]); - const [staffarr, setStaffarr] = useState([]); - const [orderheaderid, setOrderheaderid] = useState(''); - const [tenantid, setTenantid] = useState(''); - const [starttime, setStarttime] = useState(''); - const [endtime, setEndtime] = useState(''); - // const [orderstatus,setOrderstatus]=useStatus(''); - const [pendingtime, setPendingtime] = useState(''); - // const [processdate,setProcessdate]=useState(''); - const [orderdetailid, setOrderdetailid] = useState(''); - const [productid, setProductid] = useState(''); - const [categoryarr, setcategoryarr] = useState([]) - const [currentshiftobj, setCurrentshiftobj] = useState({ - shifts: 0, - assigned: 0, - remaining: 0, - shiftid: 0, - price: 0 - }); - const [tabstatus, setTabstatus] = useState(0) - const { state } = useLocation(); - const [assignedpendingcount, setAssignedpendingcount] = useState(''); - // const [, forceUpdate] = useReducer(x => x + 1, 0); - // const dispatch = useDispatch(); - const navigate = useNavigate(); - const [expandopen, setExpandopen] = useState(['', '']); - const [startdate, setStartdate] = useState('') - const [invoiceeligible, setInvoiceeligible] = useState(false) - - useEffect(() => { - console.log("categoryarr") - - console.log(orderarr, eventlocation, venuetype, starttime, endtime); - // forceUpdate(); - }, [categoryarr]) - - // const navigate = useNavigate(); - useEffect(() => { - if (state) { - setOrderheaderid(state.orderheaderid); - setTenantid(state.tenantid); - } - console.log(state) - // fetchroleslist(1) - console.log(Date.now()) - }, []) - useEffect(() => { - if (state) { - setOrderheaderid(state.orderheaderid ); - setTenantid(state.tenantid ); - } - console.log(state) - // fetchroleslist(1) - console.log(Date.now()) - }, [ - state.orderheaderid, state.tenantid - ]) - - const theme = useTheme(); - - // const fetchorderdetails = async () => { - // setLoading(true); - // await axios - // .get(`${process.env.REACT_APP_URL}/orders/orderbyid/?orderheaderid=${orderheaderid}`) - - // .then((res) => { - // console.log(res); - - // setLoading(false); - // }) - // .catch((err) => { - // console.log(err); - // setLoading(false); - // }); - - // }; - - const fetchorderaddons = async () => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL2}/orders/getordersbystatus?tenantid=${tenantid}`) - - .then((res) => { - console.log(res); - let result = res.data.Details.find((res1) => res1.orderheaderid == orderheaderid) - // orderheaderid - console.log("result") - - console.log(result) - setOrderaddons(result.orderaddons); - setVenuetype(result.venuetype) - setOtherinstructions(result.remarks) - setStartdate(result.startdate) - - - - setClientname(result.tenantname); - setEventlocation(''); - setTenantaddress(result.tenantaddress); - setEventname(result.eventname); - - - - setOrderdate(dayjs(result.orderdate).format('MM/DD/YYYY') || ''); - setOrderid(result.orderid); - setOrderstatus(result.orderstatus); - setTaxamount(result.taxamount); - setSubtotal(result.orderamount) - setGrandtotal(result.ordervalue); - setCancelleddate(dayjs(result.cancelled).format('MM/DD/YYYY') || ''); - - // if (result.orderstatus === 'pending') { - setPendingtime(result.pending) - // } - - - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - } - - - - const fetchorderattires = async () => { - setLoading(true); - await axios - .get(`${process.env.REACT_APP_URL2}/orders/getorderdetails?orderheaderid=${orderheaderid}`) - - .then((res) => { - console.log('res'); - console.log(res); - setOrderarr(res.data.Details || []); - // let result = res.data.Details.find((res1) => res1.orderheaderid == searchParams.get('id')) - // orderheaderid - // console.log(result) - // setOrderaddons(result.orderaddons); - // setVenuetype(result.venuetype) - // setOtherinstructions(result.remarks) - // console.log("res"); - // let result = _.chain(res.data.Details) - - // .groupBy("shiftid") - - // .map((value, key) => ({shiftid:key, locationaddress: value[0].locationaddress, roles: value })) - // .value() - - // setcategoryarr(result); - console.log('categoryarr'); - setcategoryarr(res.data.Details) - console.log(res.data.Details); - - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - } - - const fetchroleslist = async (cid, starttime1, endtime1, hid, sid) => { - let fromdate = dayjs(starttime1).subtract(59, 'minutes') - .format('YYYY-MM-DD HH:mm:ss'); - // let todate = dayjs(endtime1).utc().format('YYYY-MM-DD HH:mm:ss'); - // let fromdate = dayjs(starttime1).utc().format('YYYY-MM-DD HH:mm:ss'); - let todate = dayjs(endtime1) - .format('YYYY-MM-DD HH:mm:ss'); - let url1; - if (starttime1) { - url1 = `${process.env.REACT_APP_URL2}/staffs/pools/getstaffs/?categoryid=${cid}&headerid=${hid}&shiftid=${sid}&starttime=${fromdate}&endtime=${todate}`; - } else { - url1 = `${process.env.REACT_APP_URL2}/staffs/pools/getstaffs/?categoryid=${cid}&headerid=${hid}&shiftid=${sid}` - } - setLoading(true); - setStafflist([]) - await axios - // .get(`${process.env.REACT_APP_URL2}/staffs/pools/getstaffs/?categoryid=${cid}&headerid=${hid}&starttime=${fromdate}&endtime=${todate}`) - .get(url1) - .then((res) => { - console.log('rolelist'); - console.log(res); - // console.log(fromdate, todate) - // console.log(dayjs(starttime1).format('YYYY-MM-DD HH:mm:ss')) - // console.log(dayjs(endtime1).format('HH:mm:ss')) - - - setStafflist(res.data.Details || []) - // let result = res.data.Details.find((res1) => res1.orderheaderid == searchParams.get('id')) - // orderheaderid - // console.log(result) - // setOrderaddons(result.orderaddons); - // setVenuetype(result.venuetype) - // setOtherinstructions(result.remarks) - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - } - - - const fetchstafflist = async (odid) => { - setLoading(true) - try { - // await axios.get(`${process.env.REACT_APP_URL}/orders/orderanalytics?orderdate=${chosendate}`) - await axios.get(`${process.env.REACT_APP_URL2}/orders/getassignedinfo?orderdetailid=${odid}`) - - .then((res) => { - console.log(res) - if (res.data.status) { - setStafflist(res.data.Details) - } - - setLoading(false) - }).catch((err) => { - console.log(err) - setLoading(false) - }) - - } catch (err) { - console.log(err); - setLoading(false) - } - - } - const cancelorder = async () => { - - await axios.put(`${process.env.REACT_APP_URL2}/orders/cancel`, { - - // "Orderheaderid": parseInt(orderheaderid), - // "Tenantid": parseInt(tenantid), - // "Orderstatus": "cancelled", - // "Currentdatetime": dayjs().format('YYYY-MM-DD HH:mm:ss'), - // "Cod": false, - // "Remarks": "", - "orderheaderid": parseInt(orderheaderid), - // "orderdetailid":78, - // "shiftid":788, - "orderstatus": "cancelled", - "cancelled": dayjs().format('YYYY-MM-DD HH:mm:ss'), - "unserviceable": (invoiceeligible) ? 0 : 1, - - }) - .then((res) => { - console.log(res) - if (res.data.status) { - if (orderheaderid && tenantid) { - // fetchorderdetails(); - fetchorderaddons(); - fetchorderattires(); - } - } - - }).catch((err) => { - console.log(err) - }) - - } - - const unassign = async (val) => { - - let obj = { - orderheaderid: orderheaderid, - orderprocessid: val.orderprocessid, - orderdetailid: val.orderdetailid, - orderstatus: "pending", - pending: dayjs().format('YYYY-MM-DD HH:mm:ss'), - // processing:0, - // cancelled:0, - // completed:0, - // accepted:0, - status: 1 - } - console.log(obj) - - await axios.put(`${process.env.REACT_APP_URL2}/orders/updateprocessstatus`, obj) - .then((res) => { - console.log(res) - if (res.data.message === "Successful") { - // if (orderheaderid && tenantid) { - enqueueSnackbar('Role unassigned successfully', { - variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }) - if (currentshiftobj.assigned > currentshiftobj.shifts) { - sendunassignnotification(val) - } - - - - fetchorderaddons(); - fetchorderattires(); - setOpen(false); - fetchassignedcount(); - dialogclose() - setTimeout(() => { - fetchassignedcount(); - - }, 2000) - } - - }).catch((err) => { - console.log(err) - }) - - } - - - const sendunassignnotification = (val) => { - console.log(val) - let data2; - let tokenarr = [val.userfcmtoken] - let arr1 = [{ - "notificationid": 0, - "notificationdate": dayjs().format('YYYY-MM-DD HH:mm:ss'), - "Title": "Staff Un-Asigned", - "message": `${val.firstname} has been Un-assigned to the order ${orderid}`, - "configid": 2, - "tenantid": tenantid, - "orderheaderid": orderheaderid, - "orderprocessid": val.orderprocessid, - "shiftid": val.shiftid, - "userid": val.userid, - "orderid": orderid, - "success": 0, - "orderstatus": 'assigned', - "processing": dayjs().format('YYYY-MM-DD HH:mm:ss'), - "notifytype": 2, - "notifyreason": 'Staff Un-Assigned' - // "sound": "ring", - // "click_action": "FLUTTER_NOTIFICATION_CLICK", - // "firstname": val.firstname - - }]; - - - - - data2 = { - "Title": "Staff Un-Asigned", - "message": `A Staff has been Un-assigned to the order ${orderid}`, - "tenantid": tenantid, - "orderheaderid": orderheaderid, - "orderid": orderid, - "configid": 2, - // "click_action": "FLUTTER_NOTIFICATION_CLICK" - } - let fcmmodel = { - "priority": "high", - "registration_ids": tokenarr, - "data": data2, - "notification": { - "body": `An order has been Un-assigned ${orderid}`, - "title": "Legendary", - "sound": "ring", - "content_available": true, - "click_action": "FLUTTER_NOTIFICATION_CLICK" - } - } - let grpnotifyobj = { - "notifications": arr1, - "fcmmodel": fcmmodel - } - console.log("grpnotifyobj unassign") - console.log(grpnotifyobj) - sendgroupnotification(grpnotifyobj) - } - - useEffect(() => { - console.log(orderheaderid); - if (orderheaderid && tenantid) { - // fetchorderdetails(); - fetchorderaddons(); - fetchorderattires(); - fetchassignedcount() - // fetchuserdetails(); - console.log(location.state || ''); - // setOrderid(location.state.orderid || ''); - // setEventlocation(location.state.eventlocation || ''); - // setEventlocation(address || []); - - // setOrderdate(dayjs(location.state.orderdate.substring(0, 10)).format('MM/DD/YYYY') || ''); - // setDuedate(dayjs(location.state.orderdate.substring(0, 10)).format('MM/DD/YYYY') || '') - // setEventname(location.state.eventname || ''); - // setClientname(location.state.tenantname || '') - } else { - setLoading(false); - } - // fetchorderdetails(); - console.log(orderheaderid, tenantid) - }, [orderheaderid, tenantid, assignedpendingcount]); - - const handleClose = () => { - setOpen(false); - }; - const dialogclose = () => { - setDialogopen(false); - setStaffarr([]); - setExpandopen(['', '']) - }; - useEffect(() => { - console.log(currentshiftobj) - }) - - - const assignok = async () => { - let arr = [] - let arr1 = []; - staffarr.map((val) => { - arr.push({ - "orderprocessid": 0, - // "processdate": `${dayjs(new Date()).format('YYYY-MM-DD')} ${dayjs(new Date()).format('HH:mm:ss')}`, //current date - "processdate": dayjs().format('YYYY-MM-DD HH:mm:ss'), //current date - "tenantid": tenantid, - "orderheaderid": orderheaderid, - "orderdetailid": val.orderdetailid,//// - "productid": val.productid,///// - "userid": val.userid, - "orderstatus": "pending", - "pending": `${dayjs(pendingtime).format('YYYY-MM-DD')} ${dayjs(pendingtime).format('HH:mm:ss')}`, // if pending - "starttime": `${dayjs(starttime).format('YYYY-MM-DD')} ${dayjs(starttime).format('HH:mm:ss')}`, - "endtime": `${dayjs(endtime).format('YYYY-MM-DD')} ${dayjs(endtime).format('HH:mm:ss')}`, - "appuserid": parseInt(localStorage.getItem("appuserid")), //loginuserid - "shiftid": val.shiftid, - "userrate": val.userrate, - "productrate": val.productrate - }) - - staffarr.map((val) => { - arr1.push({ - "notificationid": 0, - "notificationdate": dayjs().format('YYYY-MM-DD HH:mm:ss'), - "Title": "Staff Asigned", - "message": `A Staff has been assigned to the order ${orderid}`, - "configid": 2, - "tenantid": tenantid, - "orderheaderid": orderheaderid, - "orderprocessid": 0, - "shiftid": val.shiftid, - "userid": val.userid, - "orderid": orderid, - "sound": "ring", - "click_action": "FLUTTER_NOTIFICATION_CLICK", - "firstname": val.firstname - }) - - }) - - }) - console.log('arr') - console.log(arr) - - await axios.post(`${process.env.REACT_APP_URL2}/orders/createorderprocess`, arr) - .then((res) => { - console.log(res) - if (res.data.message === "Successfully created") { - // if (orderheaderid && tenantid) { - // fetchorderdetails(); - // fetchorderaddons(); - // fetchorderattires(); - // } - - - enqueueSnackbar('Roles assigned successfully', { - variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }) - // fetchroleslist(productid, '', '', orderheaderid, arr1[0].shiftid); - console.log(productid, '', '', orderheaderid, arr1[0].shiftid) - - - - - // arr1.map((val2) => { - // notificationpush(val2,val2.Title); - // }) - setDialogopen(false); - fetchorderattires(); - fetchassignedcount(); - } - - }).catch((err) => { - console.log(err) - }) - console.log(arr) - } - - const notificationpush = async (val) => { - let fcmtoken = val.userfcmtoken - let obj1 = { - "notificationid": 0, - "notificationdate": dayjs().format('YYYY-MM-DD HH:mm:ss'), - "Title": "Staff Asigned", - "message": `A Staff has been assigned to the order ${orderid}`, - "configid": 2, - "tenantid": tenantid, - "orderheaderid": orderheaderid, - "orderprocessid": val.orderprocessid, - "shiftid": val.shiftid, - "userid": val.userid, - "orderid": orderid, - "sound": "ring", - "click_action": "FLUTTER_NOTIFICATION_CLICK", - // "firstname": val.firstname - - }; - console.log(obj1, fcmtoken) - await axios.post(`${process.env.REACT_APP_URL2}/utils/notification/send`, - { - "priority": "high", - "registration_ids": [fcmtoken], - "data": obj1, - "notification": { - "body": `A Staff has been assigned to ${orderid}`, - "title": "Legendary", - "sound": "ring", - "content_available": true - } - } - , { - headers: { - 'Authorization': `Bearer ${process.env.REACT_APP_STAFF_TOKEN}` - } - } - ) - .then((res) => { - console.log(res) - // if(res.data.status){ - enqueueSnackbar('Notification sent successfully', { - variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }) - // } - }) - .catch((err) => { - console.log(err) - }) - - } - - const fetchassignedstaffs = async () => { - - // console.log(obj1) - await axios.get(`${process.env.REACT_APP_URL2}/orders/getnotificationusers?orderheaderid=${orderheaderid}`) - .then((res) => { - if (res.data.status) { - let arr1 = []; - let data2; - let tokenarr = [] - console.log(res) - - res.data.details.map((val) => { - arr1.push({ - "notificationid": 0, - "notificationdate": dayjs().format('YYYY-MM-DD HH:mm:ss'), - "Title": "Staff Asigned", - "message": `${val.staffname} has been assigned to the order ${orderid}`, - "configid": 2, - "tenantid": tenantid, - "orderheaderid": orderheaderid, - "orderprocessid": val.orderprocessid, - "shiftid": val.shiftid, - "userid": val.userid, - "orderid": orderid, - "success": 0, - "orderstatus": 'assigned', - "processing": dayjs().format('YYYY-MM-DD HH:mm:ss'), - "notifytype": 1, - "notifyreason": 'Staff Assigned' - // "sound": "ring", - // "click_action": "FLUTTER_NOTIFICATION_CLICK", - // "firstname": val.firstname - - }); - tokenarr.push(val.userfcmtoken); - - }) - - - - data2 = { - "Title": "Staff Asigned", - "message": `A Staff has been assigned to the order ${orderid}`, - "tenantid": tenantid, - "orderheaderid": orderheaderid, - "orderid": orderid, - "configid": 2, - // "click_action": "FLUTTER_NOTIFICATION_CLICK" - } - let fcmmodel = { - "priority": "high", - "registration_ids": tokenarr, - "data": data2, - "notification": { - "body": `An order has been assigned ${orderid}`, - "title": "Legendary", - "sound": "ring", - "content_available": true, - "click_action": "FLUTTER_NOTIFICATION_CLICK" - } - } - let grpnotifyobj = { - "notifications": arr1, - "fcmmodel": fcmmodel - } - console.log("grpnotifyobj") - console.log(grpnotifyobj) - sendgroupnotification(grpnotifyobj) - // notificationpush(obj2, val.userfcmtoken); - // notificationpush(arr1,tokenarr); - // enqueueSnackbar('Notifications sent successfully', { - // variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' }, - // autoHideDuration: 2000 - // }) - } - }) - .catch((err) => { - console.log(err) - }) - - } - - const sendgroupnotification = async (obj1) => { - - console.log(obj1) - await axios.post(`${process.env.REACT_APP_URL2}/utils/notification/sendall`, obj1, { - headers: { - 'Authorization': `Bearer ${process.env.REACT_APP_STAFF_TOKEN}` - } - } - ) - .then((res) => { - console.log(res) - if (res.data.status) { - // updateorderstatus(); - enqueueSnackbar('Notification sent successfully', { - variant: 'success', anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }) - fetchorderaddons(); - } - }) - .catch((err) => { - console.log(err) - }) - - } - - // const updateorderstatus = async () => { - - // await axios.put(`${process.env.REACT_APP_URL2}/orders/updateorderstatus`,{ - // "orderheaderid":orderheaderid, - // "tenantid":tenantid, - // "orderstatus":"processing", - // "pending":"", - // "processing":dayjs().format('YYYY-MM-DD HH:mm:ss'), - // "completed":"" - // }) - // .then((res) => { - // console.log(res) - // fetchorderdetails(); - // fetchorderaddons(); - // fetchorderattires(); - - // }) - // .catch((err) => { - // console.log(err) - // fetchorderdetails(); - // fetchorderaddons(); - // fetchorderattires(); - // }) - - // } - - const fetchassignedcount = async () => { - - // console.log(obj1) - await axios.get(`${process.env.REACT_APP_URL2}/orders/getorderstatuscount?orderheaderid=${orderheaderid}`) - .then((res) => { - if (res.data.status) { - // let arr1=[]; - console.log(res) - setAssignedpendingcount(res.data.pendingcount) - fetchorderaddons() - // res.data.details.map((val) => { - // let val2={ - // "notificationid": 0, - // "notificationdate": dayjs().format('YYYY-MM-DD HH:mm:ss'), - // "Title": "Staff Asigned", - // "message": `A Staff has been assigned to the order ${orderid}`, - // "configid": 2, - // "tenantid": tenantid, - // "orderheaderid": orderheaderid, - // "orderprocessid": val.orderprocessid, - // "shiftid": val.shiftid, - // "userid": val.userid, - // "orderid": orderid, - // "sound": "ring", - // "click_action": "FLUTTER_NOTIFICATION_CLICK", - // // "firstname": val.firstname - - // }; - - - // notificationpush(val2,val.userfcmtoken); - - - // }) - } else { - setAssignedpendingcount(res.data.pendingcount) - fetchorderaddons() - } - }) - .catch((err) => { - console.log(err) - }) - - } - - function AlertCustomerDelete({ - // title, - open, handleClose }) { - const [deletepassword, setDeletepassword] = useState(''); - - return ( - handleClose(false)} - maxWidth="xs" - PaperProps={{ sx: { borderRadius: 3 } }} - > - - - - - - - - Cancel Order - - - Confirm to permanently cancel this order - - - - - - - - {orderid.slice(4)} - - - {(invoiceeligible) && - }> - Order is within 24Hrs time frame. The order will be invoiced with standard pricing as agreed. - Terms & Condition link - - } - - - Please type in the order number to confirm. - - setDeletepassword(e.target.value)} - error={deletepassword !== orderid.slice(4)} - value={deletepassword} - placeholder={orderid.slice(4)} - /> - - - - - - - - - ); - } - - return ( - <> - {loading && } - - - - - - - - - - - - - - Assign Roles - - - {clientname} ยท {currentrole} - - - - - {orderid} - - - - - - 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} - - - ))} - - - - - {/* title */} - - {/* - - - - - setTabstatus((e) => (e === 0) ? 1 : 0)} - variant="scrollable" scrollButtons="auto" > - - - - - - - - - - - - - - */} - - - {/* */} - {/* */} - {(stafflist.length === 0) ? - <> - {(loading) ? - <> - - - - - - - - : - No Staffs Available - - } - - : - - -
- - - # - Staff - - Category - - {/* Gender */} - - Price - - Experience - - Level - City - Action - - - - - - {stafflist.map((val, i) => { - - return - - - res.userid == val.userid)) ? '#f5f5f5' : '', ':hover': { - backgroundColor: (staffarr.find((res) => res.userid == val.userid)) ? '#f5f5f5 !important' : '' - } - }}> - - {i + 1} - - - - - {/* {row.tenantname.charAt(0)} */} - - - {val.firstname} - - {val.contactno} - - - - - - {/* - {val.contactno} - - some demo address - - */} - - {val.cateoryname} - - {/* {row.category} */} - - - - {val.rolecost} - {/* */} - {/* {(row.gender === 1) && } */} - {/* {row.gender === 0 && } */} - {/* */} - {/* */} - {/* B+ */} - {/* {row.devicetype} */} - {val.experience} Years - - {val.city} - - {(val.orderdetailid !== orderdetailid) ? - <> - res.userid == val.userid)) ? true : false} - onClick={(e) => { - console.log(currentshiftobj) - if (currentshiftobj.remaining >= 0) { - - - if (e.target.checked && currentshiftobj.remaining != 0) { - let arr = staffarr; - arr.push({ - userid: val.userid, - orderdetailid, - productid, - shiftid: currentshiftobj.shiftid, - userrate: currentshiftobj.price, - productrate: val.rolecost, - firstname: val.firstname - }); - setStaffarr([...arr]) - let obj = currentshiftobj; - // obj.shifts--; - // obj.assigned = arr.length; - obj.assigned++; - obj.remaining = obj.shifts - obj.assigned; - setCurrentshiftobj({ ...obj }) - } else if (currentshiftobj.assigned != currentshiftobj.shifts || (currentshiftobj.remaining === 0 && (!e.target.checked))) { - let arr = staffarr; - // let index = arr.indexOf(val.userid) - let index = arr.findIndex((val1) => val1.userid === val.userid) - arr.splice(index, 1); - setStaffarr([...arr]); - let obj = currentshiftobj; - // obj.shifts++; - // obj.assigned = arr.length; - obj.assigned--; - obj.remaining = obj.shifts - obj.assigned; - setCurrentshiftobj({ ...obj }) - } - // forceUpdate(); - console.log(staffarr); - } - }} /> - - - - : - - <> - - - {/* */} - - { - console.log(val) - unassign(val) - // sendunassignnotification(val) - }}> - {/* */} - - - - - { - console.log(val) - // unassign(val) - notificationpush(val) - }}> - {/* */} - - - - - } - - - - - {(val.orderid) && - - <> - - - - } - - - - - }) - } - -
-
- } - - - - {(stafflist.length > 0) && - <> - OK - { - - // dialogclose(); - setStaffarr([]); - let obj = currentshiftobj; - // obj.shifts = row.orderqty; - obj.remaining = obj.shifts; - obj.assigned = 0; - - setCurrentshiftobj(obj); - }}>clear - - } - { dialogclose() }}>Close - - - {/* */} - {/* */} - - - - - - {/* - - - - - - */} - {/* */} - {/* - 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 } - }} - > - - - - - - - - Order Details - - - - - {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 */} - - - - - {/*

{(dayjs().isBefore(dayjs(startdate)))?'true':'false'}

*/} - - {/* */} - {/* */} - {/* */} - {/* - - BEO - - - - */} - {/* */} - - {/* - - Due Date - - {duedate} - */} - {/* - */} - {/* */} - {/* */} - - - - - Client - {(clientname === '') ? : clientname} - - {/* {eventlocation.map((val, i) => { - return {val} - }) - - } - {eventlocation} - fafdf - dafaf - afdafafd */} - {(tenantaddress === '') ? : tenantaddress} - - - - - - - - - Event - - {(eventname === '') ? : eventname} - {/* - {(eventlocation === '') ? : eventlocation} - - : venuetype} color="primary" variant="light" size="small" sx={{ width: 'max-content' }} /> - */} - - - - - - - {categoryarr.map((val5, j) => { - - - - - - return < Fragment key={val5.locationaddress}> - - - - - - Shift {j + 1} - - - {''}{val5.locationaddress} - - {(val5.shiftstatus === 1) && - - } - - - {val5.ordercontacts.map((val11) => { - - return <> - - {val11.contactname.charAt(0).toUpperCase()} - - - }) - - } - - - - - - - - - # - Role - Start Date - End Date - Unpaid break - Count - Assigned - Attire - Price - {/* Tax */} - Amount - Action - - - - {(val5.orderdetails.length === 0) && - <> - - - - - - - - - - - - - - - - - - - - - - {/* */} - - - - - - - - - - - - - - - - - - - - - - - - - - {/* - - - */} - - - {/* */} - - } - - {/* */} - {val5.orderdetails.map((row, i) => ( - <> - - {i + 1} - {row.productname} - {/* {row.productname} - */} - - - {dayjs(row.starttime).format('MM/DD/YYYY')} - {dayjs(row.starttime).format('hh:mm A')} - - - - {' '} - - {dayjs(row.endtime).format('MM/DD/YYYY')} - {dayjs(row.endtime).format('hh:mm A')} - - - {row.unpaidbreak || 0} - - - - - - - - - - {/* < Grid container spacing={1}> - {(row.orderattires || []).map((val) => { - return - - - - - }) - - - } - */} - - ${row.price} - {/* {row.taxamount} */} - ${row.landingamount} - - - - - - - { - setStafflist([]); - setExpandopen(((expandopen[0] === j) && (expandopen[1] === i)) ? ['', ''] : [j, i]) - // expanddatafetch(row.orderheaderid); - - // fetchroleslist(row.productid, '', '', val5.orderheaderid, row.shiftid); - fetchstafflist(row.orderdetailid) - - - } - } - > - {((expandopen[0] === j) && (expandopen[1] === i)) ? - - : - } - - - - - - {/* {(orderstatus !== 'cancelled') && - <> - - { - setExpandopen(['', '']) - setCurrentrole(row.productname); - setStarttime(row.starttime); - console.log('row') - - console.log(row) - setEndtime(row.endtime); - setProductid(row.productid); - setOrderdetailid(row.orderdetailid); - if ((row.orderqty === row.supplyqty) - || (row.status == 1) - || (val5.shiftstatus == 1) - - ) { - fetchroleslist(row.productid, '', '', val5.orderheaderid, row.shiftid); - - } else { - fetchroleslist(row.productid, row.starttime, row.endtime, val5.orderheaderid, row.shiftid); - - } - - setStaffarr([]); - - - let obj = currentshiftobj; - obj.shifts = row.orderqty; - - obj.shiftid = row.shiftid; - obj.remaining = row.orderqty - row.supplyqty; - obj.assigned = row.supplyqty; - obj.price = row.price; - - setCurrentshiftobj(obj); - - setDialogopen(true); - }} > - - - - - } */} - {(orderstatus === 'cancelled') && - <> - - - } - - {(row.status === 1) && - - - - - - } - {(row.supplyqty > row.orderqty) && - - - - - - } - - - - - - - - - - - - {/* */} - - - {(stafflist.length === 0) ? - <> - {(loading) ? - <> - - - - - - - - : - <> - - No Staffs has been Assigned - - - } - - : - - -
- - - # - Staff - Start Time - End Time - Pay Rate - - {/* Category */} - Clockin - Clockout - - Hours Worked - - - - - {/* Experience */} - - {/* Level */} - {/* City */} - Status - - - - - {stafflist.map((val, i) => { - - return - - - res.userid == val.userid)) ? '#f5f5f5' : '', ':hover': { - // backgroundColor: (staffarr.find((res) => res.userid == val.userid)) ? '#f5f5f5 !important' : '' - // } - - }}> - - {i + 1} - - {/* - - - - - - {val.staffname} - - - - - */} - - - - {val.staffname} - - - - - - - - - {dayjs(val.Starttime).format('MM/DD/YYYY')} - {dayjs(val.Starttime).format('hh:mm A')} - - - - - {dayjs(val.Endtime).format('MM/DD/YYYY')} - {dayjs(val.Endtime).format('hh:mm A')} - - - {val.rolecost} - - {/* - - {val.cateoryname} - - - - */} - - - - {/* Clock In: */} - {/* Age */} - - - - - - - - {/* Clock In: */} - {/* Age */} - - - - - - - {val.hoursworked} - - - {/* - - - - - - - - - {val.tenantname} - - - - - - {val.orderid} - - - {dayjs(val.startdate).utc().format('MM/DD/YYYY')} - - - - - - {val.eventname} - - {`${val.locationaddress.slice(0, 15)}...`} - - - */} - - - - - - - {(val.orderstatus === 'pending') && - - - } - {(val.orderstatus === 'cancelled') && - - - - } - {(val.orderstatus === 'completed') && - - - } - {(val.orderstatus === 'processing') && - - } - {(val.orderstatus === 'assigned') && - - } - {(val.orderstatus === 'confirmed') && - - } - - {(val.orderstatus === 'active') && - - } - {(val.orderstatus === 'closed') && - - } - - - - - - - }) - } - -
-
- } -
- - - - -
- - - - - - ))} - - - - - - - - - - }) - } - - - - - - - {/* */} - {/* Order Addons - */} - - { - - - (orderaddons || []).map((val) => { - return - - } variant='outlined' color="error" label={val.addon} /> - - - }) - } - {/* - } variant='outlined' color="error" label="Parking Provided" /> - - - } variant='outlined' color="error" label="Meal Provided" /> - */} - {/* - } color="error" label="Like" /> - - - } color="error" label="Like" /> - - - } color="error" label="Like" /> - */} - - {/* */} - - - {/* */} - - {/* */} - - - - Sub Total: - ${(subtotal === '') ? : subtotal} - - {/* - Discount: - - 0 - - */} - - Tax: - {(taxamount === '') ? : taxamount} - - - Grand Total: - {(grandtotal === '') ? : `$${grandtotal}`} - - - - {/* */} - - - - Other Instructions: - - - {otherinstructions} - - - - {/* - - */} - - - - {/* */} - - {/* */} - - - - ); -}; - -export default Details; diff --git a/src/pages/nearle/orders/multipleOrders.js b/src/pages/nearle/orders/multipleOrders.js index b045285..b3ab2ae 100644 --- a/src/pages/nearle/orders/multipleOrders.js +++ b/src/pages/nearle/orders/multipleOrders.js @@ -1,7 +1,8 @@ /* eslint-disable no-unused-vars */ -import React, { useEffect, useState, useRef } from 'react'; -import { getAdminPricing, getTenantLocations, getTenantCustomers, createExpressBookingBulk } from 'pages/api/doormileApi'; +import React, { useEffect, useMemo, useState, useRef } from 'react'; +import { getAdminPricing, getAdminTenants, getTenantLocations, getAdminCustomers, createExpressBookingBulk } from 'pages/api/doormileApi'; import { fetchAppLocations as fetchZoneLocations } from 'pages/api/api'; +import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete'; import Papa from 'papaparse'; import * as XLSX from 'xlsx'; import dayjs from 'dayjs'; @@ -74,14 +75,48 @@ dayjs.extend(utc); const cellHeaderSx = { fontSize: 11.5, fontWeight: 700, color: '#475569', py: 0.75, px: 1 }; const cellBodySx = { fontSize: 12, py: 0.6, px: 1 }; +// GET /admin/tenants/:id/locations carries no `slots` field (no per-location +// pickup-hours endpoint in the new API) โ€” the pickup-slot Autocomplete used +// to read tenantLocation.slots, which was always undefined, leaving the +// dropdown permanently empty and the page unsubmittable. Falls back to a +// fixed 09:00-21:00 / 30-min window, same rationale as createorder1.js's +// fetchTiming(). +const buildDefaultPickupSlots = () => { + const arr = []; + for (let t = dayjs().hour(9).minute(0).second(0); t.hour() < 21 || (t.hour() === 21 && t.minute() === 0); t = t.add(30, 'm')) { + arr.push({ name: t.format('hh:mm A'), time: t.format('HH:mm') }); + } + return arr; +}; + +// Some saved locations come back with a blank locationname (live-confirmed, +// e.g. tenant 1's only location) โ€” fall back to the address rather than +// showing an empty field/option. +const locationDisplayName = (loc) => (loc?.locationname && loc.locationname.trim()) || loc?.address || 'Unnamed location'; + +// Tenants with many locations in the same city (DailyGrubs has 23, all in +// Coimbatore) made a "(city)" suffix useless for telling them apart โ€” sort +// primary-first then alphabetically; the full address is shown per-option +// instead (see renderOption on the Business Location Autocomplete below). +const sortLocations = (locations) => + [...(locations || [])].sort((a, b) => { + if (!!a.isprimary !== !!b.isprimary) return a.isprimary ? -1 : 1; + return locationDisplayName(a).localeCompare(locationDisplayName(b)); + }); + const MultipleOrders = () => { const navigate = useNavigate(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); const locationRef = useRef(null); const userid = localStorage.getItem('userid'); - // tenantid is fixed for this app โ€” comes from the logged-in session - const tenantid = localStorage.getItem('tenantid'); + // Per express-console-api.md's "Tenant scoping" section: token tenantid + // 0/null means Doormile's own staff, who can act on any tenant โ€” NOT an + // error state. A client (tenant) login carries a real tenantid and never + // needs the picker below. `tenantid` (used throughout this file already) + // now resolves to whichever the operator is currently working as. + const loggedInTenantId = localStorage.getItem('tenantid'); + const isStaffLogin = !loggedInTenantId || loggedInTenantId === '0'; // ============================== state ============================== const [locations, setLocations] = useState([]); @@ -92,6 +127,11 @@ const MultipleOrders = () => { const [appId, setAppId] = useState(0); const [locationid, setLocationid] = useState(0); const [locationValue, setLocationValue] = useState(null); + const [locationsStatus, setLocationsStatus] = useState('loading'); + const [selectedTenantId, setSelectedTenantId] = useState(isStaffLogin ? '' : loggedInTenantId); + const [staffTenantList, setStaffTenantList] = useState([]); + const [staffTenantListLoading, setStaffTenantListLoading] = useState(false); + const tenantid = selectedTenantId; const [basePrice, setBasePrice] = useState(0); const [pricePerKm, setPricePerKm] = useState(0); @@ -105,7 +145,7 @@ const MultipleOrders = () => { const [startdate, setStartdate] = useState(dayjs().format('MM-DD-YYYY')); const [selectedtime, setSelectedtime] = useState(''); - const [pickupSlotsList, setPickupSlotsList] = useState(null); + const [pickupSlotsList, setPickupSlotsList] = useState(buildDefaultPickupSlots); const [pickupSlot, setPickupSlot] = useState(null); const [otherinstructions, setOtherinstructions] = useState(''); @@ -148,7 +188,7 @@ const MultipleOrders = () => { setUsers([]); setUploadType(null); setFileName(''); - setPickupSlotsList(null); + setPickupSlotsList(buildDefaultPickupSlots()); setPickupSlot(null); setSelectedtime(''); }, [appId]); @@ -174,68 +214,104 @@ const MultipleOrders = () => { const fetchTenantPricing = async (id) => { try { const pricing = (await getAdminPricing()) || []; - const match = pricing.find((p) => p.tenantid === id); + // id comes from localStorage (always a string) but pricing.tenantid is + // numeric โ€” a strict === here never matched, so basePrice/pricePerKm/ + // minKm stayed at their 0 defaults and every order priced at โ‚น0. + const match = pricing.find((p) => String(p.tenantid) === String(id)); if (match) { setBasePrice(match.baseprice || 0); setPricePerKm(match.priceperkm || 0); setMinKm(match.basedistance || 0); } } catch (err) { - console.log('fetchTenantPricing', err); + // swallow } }; useEffect(() => { if (tenantid) fetchTenantPricing(tenantid); - }, []); + }, [tenantid]); // ============================== gettenantlocations ============================== + // Drives what shows in place of the Business Location picker. Previously + // this function was only ever called when `tenantid` was truthy, so an + // operator on a no-tenant login (the shared admin/developer account has + // tenantid: null) got a permanently empty picker with zero feedback โ€” not + // even a toast, since the function body that would show one never ran. const gettenantlocations = async (id) => { + if (!id) { + // Staff login with no tenant picked yet โ€” the picker UI covers this. + setLocationsStatus('no-tenant'); + return; + } + setLocationsStatus('loading'); try { const details = (await getTenantLocations(id)) || []; + setTenantlocations(details); if (details.length === 1) { - setTenantlocations(details); + setLocationsStatus('ready'); setPickCust(details[0]); - setLocationid(details[0].locationid); + setLocationid(details[0].tenantlocationid); setLocationValue(details[0]); - setPickupSlotsList(details[0].slots); + } else if (details.length === 0) { + setLocationsStatus('empty'); + opentoast('No saved business locations for this tenant', 'info', 3000); } else { - setTenantlocations(details); + setLocationsStatus('ready'); } } catch (err) { - console.log('gettenantlocations', err); + // doormileAxios's response interceptor rejects with error.response.data + // (not the raw axios error), so the message lives at err.message here. + setTenantlocations([]); + setLocationsStatus('error'); + opentoast(err?.message || 'Could not load business locations', 'error', 4000); } }; useEffect(() => { - if (tenantid) gettenantlocations(tenantid); + gettenantlocations(tenantid); + }, [tenantid]); + + // ============================== fetchStaffTenantList (Doormile staff picker) ============================== + // Only staff logins (loggedInTenantId blank/0) need this โ€” a client login's + // tenant is already fixed by their token and this never fetches for them. + useEffect(() => { + if (!isStaffLogin) return; + setStaffTenantListLoading(true); + getAdminTenants() + .then((tenants) => setStaffTenantList((tenants || []).filter((t) => t.status === 'Active'))) + .catch((err) => { + opentoast(err?.message || 'Could not load tenant list', 'error', 4000); + }) + .finally(() => setStaffTenantListLoading(false)); }, []); // ============================== clientdetails ============================== + // GET /admin/customers (B2C directory) โ€” not /admin/tenantcustomers, which + // is an empty, manually-added contact book with no address/coordinates. + // This resource auto-grows from real orders (see the identical fix on + // createorder1.js's Saved Contacts) and has a confirmed live shape of + // { appcustomerid, createdat, email, name, phone, totalbookings } โ€” no + // search/query param exists, so it's fetched once and filtered in-memory. const clientdetails = async () => { try { - const customers = (await getTenantCustomers()) || []; - const filtered = searchCustList - ? customers.filter( - (val) => - val.firstname?.toLowerCase().includes(searchCustList.toLowerCase()) || - val.phone?.includes(searchCustList) || - val.contactno?.includes(searchCustList) - ) - : customers; - setCustomerlist(filtered); + const customers = (await getAdminCustomers()) || []; + setCustomerlist(customers); } catch (err) { opentoast('server error', 'warning'); } }; useEffect(() => { - if (!tenantid) return; - const t = setTimeout(() => { - if (searchCustList === '' || searchCustList.length > 2) clientdetails(); - }, 250); - return () => clearTimeout(t); - }, [searchCustList, tenantid]); + if (!tenantid || !isCustomerOpen) return; + clientdetails(); + }, [tenantid, isCustomerOpen]); + + const filteredCustomerList = useMemo(() => { + const q = searchCustList.trim().toLowerCase(); + if (!q) return customerlist; + return customerlist.filter((c) => c.name?.toLowerCase().includes(q) || c.phone?.includes(searchCustList)); + }, [customerlist, searchCustList]); // ============================== totals ============================== useEffect(() => { @@ -276,27 +352,51 @@ const MultipleOrders = () => { }; // ============================== checkbox handlers ============================== - const handleCheckboxChange = async (event, customer) => { - setLoading(true); - try { - if (event.target.checked) { - const { roundedDistance, totalcharge } = await calculateDistance(customer); - setDropCust((prev) => [...prev, { ...customer, distance: roundedDistance, totalcharge }]); - } else { - setDropCust((prev) => prev.filter((c) => c.customerid !== customer.customerid)); - } - } catch (err) { - console.error('Failed to calculate distance:', err); - } finally { - setLoading(false); + // GET /admin/customers has no address/coordinates at all, so a saved + // customer can no longer be distance-calculated at selection time (that's + // what used to throw "Invalid coordinates" and silently drop the row every + // time). Add the row immediately instead โ€” its Address cell renders an + // editable AddressAutocomplete (see the Drop List table) until the + // operator picks a real address, which is what fills in lat/lng and lets + // distance/charge be computed per-row. + const handleCheckboxChange = (event, customer) => { + const rowkey = `sel-${customer.appcustomerid}`; + if (event.target.checked) { + setDropCust((prev) => { + if (prev.some((c) => c._rowkey === rowkey)) return prev; + return [ + ...prev, + { + _rowkey: rowkey, + appcustomerid: customer.appcustomerid, + firstname: customer.name || '', + contactno: customer.phone || '', + address: '', + city: '', + postcode: '', + latitude: null, + longitude: null, + distance: 0, + totalcharge: 0, + quantity: 0, + collectionamt: 0 + } + ]; + }); + } else { + setDropCust((prev) => prev.filter((c) => c._rowkey !== rowkey)); } }; + // Still used by the CSV "Continue" promotion and the Drop List remove + // button. Safe to keep calling calculateDistance here โ€” CSV rows always + // carry real latitude/longitude from the sheet, unlike Selection rows. const handleCheckboxChange1 = async (customer) => { + const rowkey = customer._rowkey; let wasSelected = false; setDropCust((prev) => { - wasSelected = prev.some((c) => c.firstname === customer.firstname); - if (wasSelected) return prev.filter((c) => c.firstname !== customer.firstname); + wasSelected = prev.some((c) => c._rowkey === rowkey); + if (wasSelected) return prev.filter((c) => c._rowkey !== rowkey); return prev; }); if (wasSelected) return; @@ -304,16 +404,55 @@ const MultipleOrders = () => { try { const { roundedDistance, totalcharge } = await calculateDistance(customer); setDropCust((prev) => { - if (prev.some((c) => c.firstname === customer.firstname)) return prev; + if (prev.some((c) => c._rowkey === rowkey)) return prev; return [...prev, { ...customer, distance: roundedDistance, totalcharge }]; }); } catch (err) { console.error('Failed to calculate distance:', err); + OpenToast(`Could not calculate distance for ${customer.firstname || 'this row'}`, 'warning', 3000); } finally { setLoading(false); } }; + // Parses the AddressAutocomplete `place` result (see createorder1.js's + // handleDropPlaceSelected for the same pattern) for one Drop List row, + // updates that row's address/coordinates, then recomputes its distance and + // charge now that coordinates finally exist. + const handleRowPlaceSelected = async (rowkey, place) => { + const parsed = { suburb: '', city: '', postcode: '' }; + place.address_components.forEach((component) => { + component.types.forEach((type) => { + switch (type) { + case 'sublocality_level_1': + case 'sublocality': + parsed.suburb = component.long_name; + break; + case 'locality': + parsed.city = component.long_name; + break; + case 'postal_code': + parsed.postcode = component.long_name; + break; + } + }); + }); + const latitude = place.geometry.location.lat(); + const longitude = place.geometry.location.lng(); + const address = place.formatted_address || ''; + + setDropCust((prev) => + prev.map((c) => (c._rowkey === rowkey ? { ...c, address, city: parsed.city, postcode: parsed.postcode, latitude, longitude } : c)) + ); + + try { + const { roundedDistance, totalcharge } = await calculateDistance({ latitude, longitude }); + setDropCust((prev) => prev.map((c) => (c._rowkey === rowkey ? { ...c, distance: roundedDistance, totalcharge } : c))); + } catch (err) { + console.error('Failed to calculate distance:', err); + } + }; + // No zone-admin-token or bulk-notify endpoint in the new API. const fetchAppAdminTokens = async () => setAdmintoken([]); @@ -337,6 +476,12 @@ const MultipleOrders = () => { receiverphone: 'contactno', 'receiveralternatephone*': 'altcontactno', receiverfulladdress: 'address', + // Best-effort โ€” no live sample sheet to confirm these are the tenant's + // actual column headers. Without them deliverycity/deliverypincode in + // the submit payload were always sent blank; if these names are wrong, + // behavior is unchanged from before (still blank), not worse. + receivercity: 'city', + receiverpincode: 'postcode', receiverlatitude: 'latitude', receiverlongitude: 'longitude', 'itemdescription*': 'description', @@ -362,8 +507,8 @@ const MultipleOrders = () => { const processData = (data, headers) => { const normalizedMap = {}; for (const key in headerMap) normalizedMap[normalizeHeader(key)] = headerMap[key]; - const mappedData = data.map((row) => { - const newRow = {}; + const mappedData = data.map((row, idx) => { + const newRow = { _rowkey: `csv-${idx}` }; for (const key in row) { const cleanKey = normalizeHeader(key); const newKey = normalizedMap[cleanKey] || cleanKey; @@ -432,11 +577,11 @@ const MultipleOrders = () => { }; // ============================== row editors ============================== - const handleQuantityChange = (customerid, value) => { - setDropCust((prev) => prev.map((c) => (c.customerid === customerid ? { ...c, quantity: Number(value) || 0 } : c))); + const handleQuantityChange = (rowkey, value) => { + setDropCust((prev) => prev.map((c) => (c._rowkey === rowkey ? { ...c, quantity: Number(value) || 0 } : c))); }; - const handleCollectionAmtChange = (customerid, value) => { - setDropCust((prev) => prev.map((c) => (c.customerid === customerid ? { ...c, collectionamt: Number(value) || 0 } : c))); + const handleCollectionAmtChange = (rowkey, value) => { + setDropCust((prev) => prev.map((c) => (c._rowkey === rowkey ? { ...c, collectionamt: Number(value) || 0 } : c))); }; // ============================== buildDeliveryTime ============================== @@ -470,30 +615,39 @@ const MultipleOrders = () => { opentoast('Add at least one drop customer', 'warning'); return; } + // A Selection-sourced row (picked from Saved Customers) starts with no + // address/coordinates until the operator fills in its Address cell โ€” + // block submit rather than send a booking with lat/lng silently at 0. + const incomplete = dropCust.find((c) => c.latitude == null || c.longitude == null); + if (incomplete) { + opentoast(`Add a delivery address for ${incomplete.firstname || 'a selected customer'} before submitting`, 'warning', 4000); + return; + } const deliverytime = buildDeliveryTime(); // POST /admin/expressbooking/bulk takes { bookings: [...] } where each - // entry is a flat booking object (tenantid, pickuplocationid, customer_*, + // entry is a flat booking object (tenantid, pickup* fields, customer_*, // delivery*, service_option, finalprice, notes, parcels[]) โ€” not the old - // per-row orders/pickup/drop shape. pickup* fields follow the doc's own - // delivery*/pickup* naming symmetry (unverified โ€” not shown in the doc's - // example, only pickuplocationid is confirmed). + // per-row orders/pickup/drop shape. // tenantid comes from the logged-in session (see the top-level `tenantid` // const above), not from pickCust โ€” pickCust is a tenant *location* // object (GET /admin/tenants/:id/locations) and was never confirmed to // carry a tenantid back-reference, so `pickCust.tenantid` was silently // undefined on every submit. + // + // Always send raw pickup* fields, never pickuplocationid โ€” live-confirmed + // pickuplocationid currently 500s on POST /admin/expressbooking (and by + // extension the bulk endpoint, same handler); a saved location's own + // address/pincode/city/lat/lng (already on pickCust) work fine sent raw. + // Note: pickCust.pincode, not .postcode โ€” that was also wrong (the real + // GET /admin/tenants/:id/locations field is `pincode`). const bookings = dropCust.map((customer) => ({ tenantid: Number(tenantid), - ...(pickCust.locationid - ? { pickuplocationid: pickCust.locationid } - : { - pickupaddress: pickCust.address || '', - pickuppincode: pickCust.postcode || '', - pickupcity: pickCust.city || '', - pickuplatitude: Number(pickCust.latitude) || 0, - pickuplongitude: Number(pickCust.longitude) || 0 - }), + pickupaddress: pickCust.address || '', + pickuppincode: pickCust.pincode || '', + pickupcity: pickCust.city || '', + pickuplatitude: Number(pickCust.latitude) || 0, + pickuplongitude: Number(pickCust.longitude) || 0, customer_phone: customer.contactno?.toString() || '', customer_name: customer.firstname || '', deliveryaddress: customer.address || '', @@ -514,17 +668,16 @@ const MultipleOrders = () => { const res = await createExpressBookingBulk(bookings); if (res.success) { opentoast('Orders Created Successfully', 'success', 2000); - navigate('/nearle/orders'); + navigate('/doormile/orders'); } else { opentoast(res?.message || 'Order creation failed. Please try again.', 'warning', 3000); } } catch (err) { - const msg = err.response - ? err.response.data?.message || `Server error (${err.response.status})` - : err.request - ? 'Network error. Check your internet connection.' - : 'Something went wrong.'; - opentoast(msg, 'error', 3000); + // doormileAxios's response interceptor rejects with error.response.data + // (not the raw axios error), so the message lives at err.message here โ€” + // err.response was always undefined at this point, so this always fell + // through to the generic "Something went wrong." fallback before. + opentoast(err?.message || 'Something went wrong.', 'error', 3000); } finally { setLoading(false); setBtnLoading(false); @@ -605,13 +758,108 @@ const MultipleOrders = () => { - {tenantLocations?.length === 1 ? ( + {locationsStatus === 'loading' ? ( + + Loading business locationsโ€ฆ + + ) : locationsStatus === 'no-tenant' && isStaffLogin ? ( + // Staff login (express-console-api.md: token tenantid + // 0/null = "Doormile's own staff... sees everything, all + // tenants") โ€” pick which tenant this bulk order is for. + option?.tenantname || ''} + isOptionEqualToValue={(option, value) => option?.tenantid === value?.tenantid} + onChange={(event, value) => setSelectedTenantId(value ? String(value.tenantid) : '')} + renderInput={(params) => ( + + )} + /> + ) : locationsStatus === 'no-tenant' ? ( + + This login has no tenant assigned โ€” sign in as a tenant user + + ) : locationsStatus === 'error' ? ( + gettenantlocations(tenantid)} + sx={{ + display: 'flex', + alignItems: 'center', + px: 1.5, + py: 1, + borderRadius: '10px', + bgcolor: '#fef2f2', + border: '1px solid #fecaca', + color: '#b91c1c', + fontWeight: 600, + fontSize: 12.5, + cursor: 'pointer' + }} + > + Could not load business locations โ€” tap to retry + + ) : locationsStatus === 'empty' ? ( + + No saved business locations for this tenant + + ) : tenantLocations?.length === 1 ? ( { - option ? `${option.locationname} (${option.suburb || option.locationsuburb || ''})` : '' - } + options={sortLocations(tenantLocations)} + getOptionLabel={(option) => (option ? locationDisplayName(option) : '')} + renderOption={(props, option) => ( + + + + + {locationDisplayName(option)} + + {option.isprimary && ( + + PRIMARY + + )} + + + {option.address || option.city || ''} + + + + )} value={locationValue} onChange={(event, value, reason) => { if (reason === 'clear' || !value) { setLocationid(0); setLocationValue(null); setPickCust(null); - setPickupSlotsList(null); } else { - setLocationid(value.locationid || 0); + setLocationid(value.tenantlocationid || 0); setLocationValue(value); setPickCust(value); - setPickupSlotsList(value?.slots || null); } }} renderInput={(params) => ( @@ -746,7 +1020,7 @@ const MultipleOrders = () => { const finalDateTime = dayjs(`${startdate} ${formattedTime}`, 'MM-DD-YYYY hh:mm A').format('YYYY-MM-DD hh:mm A'); setPickupSlot(finalDateTime); }} - getOptionLabel={(option) => (option ? `${option.name} (${dayjs(option.time, 'HH:mm').format('hh:mm A')})` : '')} + getOptionLabel={(option) => (option ? option.name : '')} renderInput={(params) => ( { {dropCust.map((customer, index) => ( - + {index + 1} {customer.firstname} - - - - {customer.address} - - + + {customer.latitude == null || customer.longitude == null ? ( + + setDropCust((prev) => prev.map((c) => (c._rowkey === customer._rowkey ? { ...c, address: text } : c))) + } + onPlaceSelected={(place) => handleRowPlaceSelected(customer._rowkey, place)} + bias={pickCust?.latitude ? { lat: pickCust.latitude, lng: pickCust.longitude } : undefined} + textFieldSx={{ '& .MuiOutlinedInput-root': { height: 30, fontSize: 12, borderRadius: '8px' } }} + /> + ) : ( + + + {customer.address} + + + )} {uploadType === 0 ? ( @@ -1212,7 +1500,7 @@ const MultipleOrders = () => { size="small" type="number" value={customer.quantity || ''} - onChange={(e) => handleQuantityChange(customer.customerid, e.target.value)} + onChange={(e) => handleQuantityChange(customer._rowkey, e.target.value)} inputProps={{ min: 0 }} sx={{ width: 64, '& .MuiOutlinedInput-root': { borderRadius: '8px', height: 30 } }} /> @@ -1229,7 +1517,7 @@ const MultipleOrders = () => { placeholder="0" onChange={(e) => { const v = Number(e.target.value); - handleCollectionAmtChange(customer.customerid, v > 0 ? v : 0); + handleCollectionAmtChange(customer._rowkey, v > 0 ? v : 0); }} inputProps={{ min: 0 }} InputProps={{ startAdornment: โ‚น }} @@ -1237,7 +1525,7 @@ const MultipleOrders = () => { /> )} - {customer.distance} + {customer.distance || 'โ€”'} โ‚น{Number(customer?.totalcharge || 0).toFixed(2)} @@ -1477,7 +1765,7 @@ const MultipleOrders = () => { autoFocus fullWidth id="input-search-header" - placeholder="Search by name, contact, or addressโ€ฆ" + placeholder="Search by name or phoneโ€ฆ" value={searchCustList} onChange={(e) => setSearchCustList(e.target.value)} sx={{ bgcolor: 'white', borderRadius: '10px', '& .MuiOutlinedInput-input': { p: '10px 14px' } }} @@ -1498,25 +1786,28 @@ const MultipleOrders = () => { - {customerlist?.length === 0 ? ( + {filteredCustomerList?.length === 0 ? ( ) : ( - {customerlist?.map((customer, index) => { - const checked = dropCust.some((c) => c.customerid === customer.customerid); + {filteredCustomerList?.map((customer, index) => { + const rowkey = `sel-${customer.appcustomerid}`; + const checked = dropCust.some((c) => c._rowkey === rowkey); return ( - + handleCheckboxChange(event, customer)} />} label={ - {customer.firstname} ({customer.contactno}) + {customer.name} ({customer.phone}) + + + Address not on file โ€” add one after selecting - {customer.address} } /> diff --git a/src/pages/nearle/orders/optimisedOrderPreview.js b/src/pages/nearle/orders/optimisedOrderPreview.js index 2071f6f..214943e 100644 --- a/src/pages/nearle/orders/optimisedOrderPreview.js +++ b/src/pages/nearle/orders/optimisedOrderPreview.js @@ -135,7 +135,6 @@ const OptimisedOrderPreview = () => { }); setFinalDeliveryList(updateDeliveryAmtList); - console.log('finaldeliveryList', updateDeliveryAmtList); }, [deliverylist]); // ==============================|| fetchPaymentType ||============================== // @@ -187,10 +186,19 @@ const OptimisedOrderPreview = () => { const createFinalDeliveryMutation = useMutation({ mutationFn: finalCreatedeliveries, // for optimised delivery create - onSuccess: (data, variables) => { - console.log('data', data); - console.log('varialbles', variables); - notifyRiderMutation.mutate(rider.userfcmtoken || riderToken); // Call notifyRider after success + onSuccess: (data) => { + // rider state here is never set (no setRider call anywhere in this + // file), so rider.userfcmtoken threw on every success โ€” also the + // wrong argument type regardless (notifyRider needs a milerprofileid, + // not an FCM token; see dispatch/Preview.js's identical fix). Notify + // every rider actually present in the committed deliveries instead. + const notifiedRiderIds = new Set(); + (finaldeliveryList || []).forEach((order) => { + const riderId = order.userid; + if (riderId == null || notifiedRiderIds.has(String(riderId))) return; + notifiedRiderIds.add(String(riderId)); + notifyRiderMutation.mutate(riderId); + }); if (data.status == 'accepted') { OpenToast('Delivery Created Successfully', 'success', 2000); } diff --git a/src/pages/nearle/orders/orders.js b/src/pages/nearle/orders/orders.js index 52d48e2..fb38cbd 100644 --- a/src/pages/nearle/orders/orders.js +++ b/src/pages/nearle/orders/orders.js @@ -1,7 +1,7 @@ /* eslint-disable no-unused-vars */ import * as React from 'react'; import { enqueueSnackbar } from 'notistack'; -import { useState, useEffect, Fragment, useRef } from 'react'; +import { useState, useEffect, useMemo, useCallback, Fragment, useRef } from 'react'; import dayjs from 'dayjs'; var utc = require('dayjs/plugin/utc'); dayjs.extend(utc); @@ -29,7 +29,10 @@ import { CircularProgress, InputBase, Backdrop, - TableContainer + TableContainer, + Divider, + Chip, + Checkbox } from '@mui/material'; import { MdAccessTime, @@ -48,16 +51,18 @@ import { MdReceiptLong, MdClear, MdNotes, - MdMyLocation + MdMyLocation, + MdOutlineFlag, + MdPerson } from 'react-icons/md'; import { DeleteOutlined } from '@ant-design/icons'; import Loader from 'components/Loader'; import DateFilterDialog from 'components/DateFilterDialog'; import CircularLoader from 'components/CircularLoader'; import AiImage from '../../../assets/images/aiImage.png'; -import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query'; +import { useQuery, useMutation } from '@tanstack/react-query'; import { useNavigate, useLocation } from 'react-router-dom'; -import { fetchPercentageData, createAutomationDeliveries, getallriders, fetchorderscount } from '../../api/api'; +import { fetchPercentageData, createAutomationDeliveries, getallriders } from '../../api/api'; import { getBookings, cancelBooking, getAdminCustomers, getBookingTrack } from 'pages/api/doormileApi'; // ============================================================================ @@ -105,9 +110,17 @@ const ROW_STATUS_META = { }; // Top-level pill tabs. GET /admin/bookings has no documented status filter -// param, so this tab doesn't narrow the server query โ€” it's cosmetic until -// the API supports it. -const ORDERS_STATUS_TABS = [{ idx: 0, status: 'created', label: 'Bookings', color: BRAND, icon: MdLocalShipping, countKey: 'created' }]; +// param, so bookings are fetched in bulk (up to the API's pagesize cap) and +// filtered/counted client-side by row.status โ€” see the bulk fetch + useMemo +// below. `status` values here are the raw, lowercased GET /admin/bookings +// enum (matches ROW_STATUS_META's keys), not api.js's separate generic +// pending/accepted/delivered mapping used by the Deliveries page. +const ORDERS_STATUS_TABS = [ + { idx: 0, status: 'pending_pickup', label: 'Pending', color: '#f59e0b', icon: MdHourglassEmpty }, + { idx: 1, status: 'converted_to_consignment', label: 'Assigned', color: '#6366f1', icon: MdCheckCircle }, + { idx: 2, status: 'delivered', label: 'Delivered', color: '#10b981', icon: MdCheckCircle }, + { idx: 3, status: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: MdCancel } +]; // Haversine straight-line distance in km โ€” GET /admin/bookings has no `kms` // field, but does carry both pickup*/delivery* lat/lng, so this is computed @@ -198,14 +211,21 @@ const Orders = () => { const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); const [tabvalue, setTabvalue] = useState(0); - const [tabstatus, setTabstatus] = useState('Created'); - const [currentStatus, setCurrentStatus] = useState('created'); + const [tabstatus, setTabstatus] = useState('Pending'); + const [currentStatus, setCurrentStatus] = useState('pending_pickup'); + // Orders selected via the checkbox column (Pending tab only) to send to + // the AI dispatch optimiser. Assign Orders used to submit every visible + // row unconditionally, including cancelled ones (no filtering existed at + // all) โ€” this makes assignment an explicit, scoped action instead. + const [selectedOrders, setSelectedOrders] = useState([]); const [cancelOpen, setCancelOpen] = useState(false); const [trackBookingId, setTrackBookingId] = useState(null); - // GET /admin/bookings/:id/track โ€” response shape isn't documented beyond - // the endpoint existing (Status: Done in jupiter2doormile.md). Rendered - // defensively below rather than assuming specific field names. + // GET /admin/bookings/:id/track โ€” confirmed live shape (not documented in + // express-console-api.md): { booking: {...full booking incl. parcels[], + // serviceoptions[], payments[]}, assignments: [], riders: [] }. assignments/ + // riders field names beyond that haven't been observed non-empty yet, so + // they're rendered defensively (see the Track Order dialog below). const { data: trackData, isLoading: trackLoading } = useQuery({ queryKey: ['booking-track', trackBookingId], queryFn: () => getBookingTrack(trackBookingId), @@ -241,6 +261,7 @@ const Orders = () => { setTabstatus(tab.label); setCurrentStatus(tab.status); setPage(0); + setSelectedOrders([]); }; // React Queries @@ -255,14 +276,6 @@ const Orders = () => { refetchInterval: 15000 }); - const { data: ordersCountData, refetch: orderscountRefetch } = useQuery({ - queryKey: ['ordersCount', locationId, startdate, enddate, currentStatus, tid, locationId], - queryFn: fetchorderscount, - refetchOnMount: true, - refetchOnWindowFocus: true, - refetchInterval: 15000 - }); - const { data: autoRiders } = useQuery({ queryKey: ['getallriders'], queryFn: getallriders, @@ -299,8 +312,8 @@ const Orders = () => { autoHideDuration: 2000 }); refetchOrders(); - orderscountRefetch(); percentagedataRefetch(); + setSelectedOrders((prev) => prev.filter((o) => o.bookingid !== orderheaderid)); setCancelOpen(false); } else { enqueueSnackbar(res.message || 'Failed to cancel order', { @@ -310,7 +323,6 @@ const Orders = () => { }); } } catch (err) { - console.log(err); enqueueSnackbar(err.response?.data?.message || err.message || 'Failed to cancel order', { variant: 'error', anchorOrigin: { vertical: 'top', horizontal: 'right' }, @@ -319,31 +331,62 @@ const Orders = () => { } }; - // Maps to /admin/bookings โ€” tenantid/locationid/status/date filters used by - // the old endpoint aren't documented on this resource; pageno/pagesize are. - const fetchOrders = async ({ pageParam = 1 }) => { - const data = (await getBookings(pageParam, rowsPerPage)) || []; - return { - data, - nextPage: data.length === rowsPerPage ? pageParam + 1 : undefined - }; - }; - + // GET /admin/bookings has no status/date/search filter params documented + // (see express-console-api.md's "Conventions") โ€” fetched in one bulk call + // (up to the API's pagesize cap) and filtered/counted entirely client-side. + // Same approach api.js's getDeliveryStatusCounts already uses for the + // Deliveries page's chip counts, and for the same reason: cancelled orders + // used to show up mixed into every tab (there was no filtering at all โ€” + // the single "Bookings" tab that existed before was cosmetic), and the + // Assign Orders count/submit used every visible row unconditionally. const { - data: rowdata, - fetchNextPage, + data: allBookings = [], isLoading: isLoadingGetOrders, - hasNextPage, - isFetchingNextPage, refetch: refetchOrders - } = useInfiniteQuery({ - queryKey: [tabstatus, startdate, enddate, page, rowsPerPage, debouncedSearch, locationId], - queryFn: fetchOrders, - getNextPageParam: (lastPage) => lastPage.nextPage, + } = useQuery({ + queryKey: ['ordersBulk'], + queryFn: async () => (await getBookings(1, 1000)) || [], refetchInterval: 15000 }); - const rows = rowdata ? rowdata.pages.flatMap((p) => p.data) : []; + // Tab counts computed from the exact same data the table renders, so a + // tab's badge always matches what clicking it actually shows. + const statusCounts = useMemo(() => { + const counts = {}; + allBookings.forEach((b) => { + const status = String(b.status || '').toLowerCase(); + counts[status] = (counts[status] || 0) + 1; + }); + return counts; + }, [allBookings]); + + const filteredRows = useMemo(() => { + const q = debouncedSearch.trim().toLowerCase(); + return allBookings.filter((b) => { + if (String(b.status || '').toLowerCase() !== currentStatus) return false; + if (!q) return true; + return ( + b.bookingno?.toLowerCase().includes(q) || + b.pickupaddress?.toLowerCase().includes(q) || + b.deliveryaddress?.toLowerCase().includes(q) || + b.notes?.toLowerCase().includes(q) + ); + }); + }, [allBookings, currentStatus, debouncedSearch]); + + // "Load more" now reveals more of the already-fetched, already-filtered + // set instead of requesting another server page (the bulk fetch above + // already has everything) โ€” the IntersectionObserver/scroll-triggered + // reveal below is otherwise unchanged from the previous pagination UX. + const [visibleCount, setVisibleCount] = useState(rowsPerPage); + useEffect(() => { + setVisibleCount(rowsPerPage); + }, [currentStatus, debouncedSearch, rowsPerPage]); + + const rows = filteredRows.slice(0, visibleCount); + const hasNextPage = filteredRows.length > visibleCount; + const isFetchingNextPage = false; + const fetchNextPage = useCallback(() => setVisibleCount((v) => v + rowsPerPage), [rowsPerPage]); useEffect(() => { if (!hasNextPage) return; @@ -384,10 +427,12 @@ const Orders = () => { autoHideDuration: 2000, anchorOrigin: { vertical: 'top', horizontal: 'right' } }); - orderscountRefetch(); refetchOrders(); setCreateloader(false); - navigate('/nearle/dispatch/preview', { + // Assigned bookings' status flips away from pending_pickup server-side, + // so refetching above is what makes them drop out of the Pending tab. + setSelectedOrders([]); + navigate('/doormile/dispatch/preview', { state: { dispatchPreviewData: data, aiMode: 1, @@ -411,7 +456,7 @@ const Orders = () => { }); const handleCreateDelivery = async () => { - if (rows.length === 0) return; + if (selectedOrders.length === 0) return; setIsLoading(true); setCreateloader(true); // The optimiser service (routes.workolik.com) itself is untouched, but the @@ -420,7 +465,10 @@ const Orders = () => { // object (see pickuplatitude/deliverylatitude/serviceoptions[0].estimatedprice // instead). The solver's own expected input contract is unverified since // it's a separate, unchanged service. - const deliveryData = rows.map((val) => { + // Only the operator's checked selection is submitted โ€” this used to map + // over every visible row unconditionally, which (before status filtering + // existed) meant cancelled orders could get sent to the solver too. + const deliveryData = selectedOrders.map((val) => { const charge = val.serviceoptions?.[0]?.estimatedprice; return { ...val, @@ -470,6 +518,10 @@ const Orders = () => { } ]; + // Table column count for skeleton rows / colSpan โ€” one more than usual + // while the checkbox column is showing (Pending tab only). + const colCount = currentStatus === 'pending_pickup' ? 12 : 11; + return ( {(fetchpercentageIsLoading || isLoadingGetOrders || isLoading || createloader) && ( @@ -479,7 +531,7 @@ const Orders = () => { )} - {rows.length > 0 && currentStatus === 'created' && ( + {selectedOrders.length > 0 && currentStatus === 'pending_pickup' && ( )} @@ -606,7 +658,7 @@ const Orders = () => { {ORDERS_STATUS_TABS.map((t) => { const Icon = t.icon; const active = tabvalue === t.idx; - const count = ordersCountData?.[t.countKey] ?? 0; + const count = statusCounts[t.status] ?? 0; return ( { } }} > + {currentStatus === 'pending_pickup' && ( + + 0 && selectedOrders.length < rows.length} + checked={rows.length > 0 && selectedOrders.length === rows.length} + onChange={(e) => setSelectedOrders(e.target.checked ? rows : [])} + /> + + )} # Order Pickup @@ -737,7 +800,7 @@ const Orders = () => { rows.length === 0 && Array.from({ length: 10 }).map((_, idx) => ( - {Array.from({ length: 11 }).map((__, ci) => ( + {Array.from({ length: colCount }).map((__, ci) => ( @@ -747,7 +810,7 @@ const Orders = () => { {!isLoadingGetOrders && rows.length === 0 && ( - + { > - No {currentStatus} orders + No {tabstatus.toLowerCase()} orders {searchword ? 'Try a different keyword or clear the search.' : 'Adjust the location, status, or date range above.'} @@ -793,6 +856,7 @@ const Orders = () => { const customer = customerMap?.get(row.appcustomerid); const kms = haversineKm(row.pickuplatitude, row.pickuplongitude, row.deliverylatitude, row.deliverylongitude); const isCancellable = !['Cancelled', 'Converted_To_Consignment'].includes(row.status); + const isChecked = selectedOrders.some((o) => o.bookingid === row.bookingid); return ( { } }} > + {currentStatus === 'pending_pickup' && ( + + { + setSelectedOrders((prev) => + e.target.checked ? [...prev, row] : prev.filter((o) => o.bookingid !== row.bookingid) + ); + }} + onClick={(e) => e.stopPropagation()} + /> + + )} {page * rowsPerPage + index + 1} @@ -955,7 +1034,7 @@ const Orders = () => { - {currentStatus === 'created' && isCancellable && ( + {currentStatus === 'pending_pickup' && isCancellable && ( { {rows.length !== 0 && ( - + {isFetchingNextPage || hasNextPage ? ( <> @@ -1070,26 +1149,158 @@ const Orders = () => { {/* ============================================= || Track Order || ============================================= */} - setTrackBookingId(null)} maxWidth="xs" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}> + setTrackBookingId(null)} maxWidth="sm" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}> Track Order {trackLoading ? ( - ) : trackData ? ( - - {Object.entries(trackData).map(([key, value]) => ( - - - {key} - - - {typeof value === 'object' ? JSON.stringify(value) : String(value ?? 'โ€”')} - + ) : trackData?.booking ? ( + (() => { + const b = trackData.booking; + const assignments = trackData.assignments || []; + const riders = trackData.riders || []; + const fmt = (d) => (d ? dayjs(d).format('DD MMM, hh:mm A') : 'โ€”'); + const SectionLabel = ({ children }) => ( + + {children} + + ); + return ( + + + + + {b.bookingno || `#${b.bookingid}`} + + + Booked {fmt(b.createdat)} + + + + + + + + + + + + Pickup + + {b.pickupaddress || 'โ€”'} + + {b.pickuppincode && ( + + Pincode {b.pickuppincode} + + )} + + + + + + Delivery + + {b.deliveryaddress || 'โ€”'} + + {(b.deliverypincode || b.deliverycity) && ( + + {[b.deliverycity, b.deliverypincode].filter(Boolean).join(' ยท ')} + + )} + + + {b.notes && ( + + + + Notes + {b.notes} + + + )} + + + {b.parcels?.length > 0 && ( + <> + + + Parcels + + {b.parcels.map((p) => ( + } + label={`${p.itemcategory || 'Item'} ยท โ‚น${Number(p.declaredvalue || 0).toFixed(2)}`} + sx={{ bgcolor: tint(BRAND), border: `1px solid ${edge(BRAND)}`, fontWeight: 600 }} + /> + ))} + + + + )} + + {b.serviceoptions?.length > 0 && ( + <> + + + Service + {b.serviceoptions.map((s, i) => ( + + + {s.servicetype || 'โ€”'} + + + + ETA {fmt(s.estimateddeliveryat)} + + + โ‚น{Number(s.estimatedprice || 0).toFixed(2)} + + + + ))} + + + )} + + + + + Rider + {riders.length === 0 && assignments.length === 0 ? ( + + Not assigned to a rider yet. + + ) : ( + + {riders.map((r, i) => ( + + + + {r.displayname || r.name || r.phone || JSON.stringify(r)} + + + ))} + {assignments.map((a, i) => ( + + + {a.status || a.assignmentstatus || 'Assignment'} + + + {fmt(a.createdat || a.assignedat)} + + + ))} + + )} + - ))} - + ); + })() ) : ( No tracking data available for this order yet. diff --git a/src/pages/nearle/reports/mapWithRoute.js b/src/pages/nearle/reports/mapWithRoute.js index 3981871..c450dee 100644 --- a/src/pages/nearle/reports/mapWithRoute.js +++ b/src/pages/nearle/reports/mapWithRoute.js @@ -30,8 +30,6 @@ const endIcon = new L.Icon({ }); const MapWithRoute = ({ coordinates, additionalProps, order, setMapOpen }) => { - console.log('additionalProps', additionalProps); - const mapRef = useRef(null); const [routePoints, setRoutePoints] = useState([]); const [loading, setLoading] = useState(false); diff --git a/src/pages/nearle/reports/ordersDetails.js b/src/pages/nearle/reports/ordersDetails.js index 39ae86c..cbc6288 100644 --- a/src/pages/nearle/reports/ordersDetails.js +++ b/src/pages/nearle/reports/ordersDetails.js @@ -1,4 +1,4 @@ -import { React, useState, useEffect, useRef } from 'react'; +import { React, useState, useEffect, useMemo, useRef } from 'react'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; // material-ui @@ -66,7 +66,7 @@ import { FaCircleCheck } from 'react-icons/fa6'; import MapWithRoute from './mapWithRoute'; import CircularLoader from 'components/CircularLoader'; import { fetchDeliveries, fetchRidersList, gettenantlocations, getTenants } from 'pages/api/api'; -import { getDashboard, getConsignmentLogs } from 'pages/api/doormileApi'; +import { getConsignmentLogs } from 'pages/api/doormileApi'; import { CSVExport } from 'components/third-party/ReactTable'; import Loader from 'components/Loader'; import { enqueueSnackbar } from 'notistack'; @@ -117,6 +117,10 @@ const edge = (c) => a(c, '55'); const BRAND = '#C01227'; const BRAND_LIGHT = '#D25463'; +// Haversine-derived kms come back with long floating-point tails +// (8.47316323423434532 km) โ€” round to 2 decimals for display everywhere. +const formatKm = (value) => Number(value || 0).toFixed(2); + const SoftPaper = (props) => ( { setTenantid(0); @@ -353,15 +334,23 @@ export default function OrdersDetails() { calculateTotalDistance(coData); setMapOpen(true); } catch (error) { - console.log('getdeliverylogs', error); opentoast('No Logs Found ', 'error', 2000); } finally { setLogsLoading(false); } }; - // ==============================|| fetchDeliveries (infinite) ||============================== // - + // ==============================|| fetchDeliveries (bulk, auto-drained) ||============================== // + // GET /admin/bookings has no status/date/tenant/location/rider filter + // params (see express-console-api.md's "Conventions") โ€” fetchDeliveries in + // api.js only ever reads rowsPerPage off this query's key, so switching + // tabs/tenant/rider used to change nothing about which rows were fetched, + // only the cache key. The tab count badges were even further disconnected + // โ€” sourced from GET /admin/dashboard, which also takes no params, so they + // never matched the (also unfiltered) table either. Fixed the same way + // deliveries.js already was: auto-drain every page into one client-side + // dataset, then filter/count from that single source so the tab badge and + // the table can never disagree. const { data: deliveriesData, isLoading: fetchDeliveriesIsLoading, @@ -371,26 +360,54 @@ export default function OrdersDetails() { hasNextPage, isFetchingNextPage } = useInfiniteQuery({ - queryKey: [ - 'fetchdeliveries', - appId, - userid, - currentStatus, - startdate, - enddate, - rowsPerPage, - debouncedSearch, - tenantid, - locationid, - selectedRider?.userid || 0 - ], + queryKey: ['fetchdeliveries-all', appId, userid, 'all', startdate, enddate, 200, '', 0, 0, 0], queryFn: fetchDeliveries, getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined, refetchOnWindowFocus: true, refetchOnMount: true, refetchOnReconnect: true }); - const rows = deliveriesData?.pages.flatMap((page) => page.rows) || []; + const allRows = useMemo(() => (deliveriesData?.pages || []).flatMap((page) => page.rows || []), [deliveriesData]); + + // Unconditional drain โ€” keeps pulling pages regardless of scroll position, + // so counts/filters are accurate even before the operator scrolls. + useEffect(() => { + if (hasNextPage && !isFetchingNextPage) fetchNextPage(); + }, [hasNextPage, isFetchingNextPage, fetchNextPage, allRows.length]); + + const rows = useMemo(() => { + const wantStatus = String(currentStatus || '').toLowerCase(); + const wantTenantId = Number(tenantid) || 0; + const wantRiderId = Number(selectedRider?.userid) || 0; + const q = String(debouncedSearch || '').trim().toLowerCase(); + return allRows.filter((r) => { + if (wantStatus !== 'all' && String(r.orderstatus || '').toLowerCase() !== wantStatus) return false; + if (wantTenantId && Number(r.tenantid) !== wantTenantId) return false; + if (wantRiderId && Number(r.userid) !== wantRiderId) return false; + if (q) { + const hay = [r.deliverycustomer, r.deliveryaddress, r.pickupcustomer, r.pickupaddress, r.orderid, r.tenantname, r.ridername] + .map((v) => String(v || '').toLowerCase()) + .join(' '); + if (!hay.includes(q)) return false; + } + return true; + }); + }, [allRows, currentStatus, tenantid, selectedRider, debouncedSearch]); + + // Sourced from the exact same allRows the table renders, so a tab's badge + // always matches what clicking it actually shows. + const statusCountByKey = useMemo(() => { + const c = { all: allRows.length, pending: 0, accepted: 0, arrived: 0, picked: 0, active: 0, delivered: 0, skipped: 0, cancelled: 0 }; + allRows.forEach((r) => { + const s = String(r.orderstatus || '').toLowerCase(); + if (c[s] !== undefined) c[s] += 1; + }); + return c; + }, [allRows]); + const total = statusCountByKey.all; + const pendingLenght = statusCountByKey.pending; + const deliveredLenght = statusCountByKey.delivered; + const cancelLenght = statusCountByKey.cancelled; useEffect(() => { if (!hasNextPage) return; @@ -456,33 +473,6 @@ export default function OrdersDetails() { enabled: tenantid !== 0 }); - // ==============================|| status summary counts ||============================== // - const fetchcount = async () => { - setIsLoading(true); - try { - const data = (await getDashboard()) || {}; - settotal(data.total || 0); - setPendingLenght(data.pending || 0); - setAssignLenght(data.accepted || 0); - setArrivedLenght(data.arrived || 0); - setPickedLenght(data.picked || 0); - setActiveLenght(data.active || 0); - setDeliveredLenght(data.delivered || 0); - setSkippedLenght(data.skipped || 0); - setCancelLenght(data.cancelled || 0); - } catch (err) { - enqueueSnackbar(err.message, { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - } finally { - setIsLoading(false); - } - }; - useEffect(() => { - fetchcount(); - }, [appId, startdate, enddate, currentStatus, tenantid, locationid, selectedRider]); // CSV export payload โ€” flat schema preserved for backwards compatibility. const csvData = rows?.map((order) => ({ @@ -556,7 +546,7 @@ export default function OrdersDetails() { return ( <> - {(isLoading || + {(fetchDeliveriesIsLoading || fetchtenantsIsLoading || fetchlocationsIsLoading || logsLoading || @@ -609,7 +599,7 @@ export default function OrdersDetails() { value={item.value ?? 0} icon={} color={item.color} - loading={isLoading} + loading={fetchDeliveriesIsLoading} /> ); @@ -1198,19 +1188,19 @@ export default function OrdersDetails() { } - label={cancelled || row.kms == '' ? '0 km' : `${row.kms} km`} + label={cancelled || row.kms == '' ? '0.00 km' : `${formatKm(row.kms)} km`} tooltip="KMS" /> } - label={`${row.cumulativekms ?? 0} km`} + label={`${formatKm(row.cumulativekms)} km`} tooltip="Actual KMS" /> } - label={`${row.previouskms || (cancelled ? '0.00' : row.kms) || 0} km`} + label={`${formatKm(row.previouskms || (cancelled ? 0 : row.kms))} km`} tooltip="Rider KMS" /> @@ -1508,12 +1498,17 @@ export default function OrdersDetails() { - + {/* GET /admin/bookings has no per-event timestamps (accepted/ + arrived/pickup/start/cancel) โ€” only assigntime (updatedat) and + deliverydate exist on the mapped row (see fetchDeliveries in + api.js), so row.pickuptime is always undefined and this cell + always renders the loading skeleton. The old `success` check + compared against a `deliverystatus` field that never existed on + this row either (the real field is `orderstatus`) โ€” dropped + rather than guessed, since orderstatus's confirmed values + (pending/accepted/active/delivered/cancelled) have no distinct + "picked" state to map it to. */} + @@ -1554,19 +1549,19 @@ export default function OrdersDetails() { } - label={cancelled || row.kms == '' ? '0 km' : `${row.kms} km`} + label={cancelled || row.kms == '' ? '0.00 km' : `${formatKm(row.kms)} km`} tooltip="KMS" /> } - label={`${row.cumulativekms ?? 0} km`} + label={`${formatKm(row.cumulativekms)} km`} tooltip="Actual KMS" /> } - label={`${row.previouskms || (cancelled ? '0.00' : row.kms) || 0} km`} + label={`${formatKm(row.previouskms || (cancelled ? 0 : row.kms))} km`} tooltip="Rider KMS" /> diff --git a/src/pages/nearle/reports/profitability.js b/src/pages/nearle/reports/profitability.js new file mode 100644 index 0000000..6a72123 --- /dev/null +++ b/src/pages/nearle/reports/profitability.js @@ -0,0 +1,187 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useInfiniteQuery } from '@tanstack/react-query'; + +// material-ui +import { Box, Paper, TextField, Typography } from '@mui/material'; +import { MdCalendarMonth } from 'react-icons/md'; + +import dayjs from 'dayjs'; + +import { fetchDeliveries } from 'pages/api/api'; +import Loader from 'components/Loader'; +import PageHeader from 'components/nearle_components/PageHeader'; +import ProfitabilitySection, { + BASE_REVENUE, + BASE_KM_LIMIT, + EXTRA_RATE_KM, + VARIABLE_RATE_KM, + FIXED_COST_PER_SLOT, + BATCHES_DEFAULT +} from '../dispatch/ProfitabilitySection'; +import { SKIPPED_STATUSES } from '../dispatch/dispatchShared'; + +// ============================================================================ +// Standalone Profitability report โ€” reference project (nearle_console_express) +// has this as its own page (reports/profitability.js); here it only existed +// embedded inside Dispatch.js's live map. Rather than rebuild the UI (the +// reference re-implements the profit math inline in raw MUI), this reuses +// the already-built, already-tested ProfitabilitySection.js component as-is +// and just supplies it with independently-fetched data โ€” mirrors how the +// reference page ALSO re-fetches and re-groups independently rather than +// reusing its own Dispatch page's pipeline (which is entangled with +// Leaflet/GPS/zone-bucketing state not needed for a report). +// +// GET /admin/bookings has no server-side date/zone/status filter params (see +// src/pages/api/CLAUDE.md) โ€” bulk-fetched and grouped entirely client-side, +// same pattern as reports/ordersDetails.js and deliveries.js. +// ============================================================================ + +const DT = { + radiusCard: 14, + shadowSoft: '0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.05)', + textSecondary: '#64748b', + borderSubtle: '#e2e8f0', + surface: '#ffffff' +}; +const BRAND = '#C01227'; + +// Profit formulas + batch windows imported from ProfitabilitySection.js +// (the single source of truth) rather than duplicated here โ€” used to total +// across all riders for the daily KPI chip; per-rider numbers are computed +// by ProfitabilitySection itself from the same `riders` data, so this must +// match exactly or the chip and the table below it would disagree. +const orderRevenue = (km) => (km <= BASE_KM_LIMIT ? BASE_REVENUE : BASE_REVENUE + (km - BASE_KM_LIMIT) * EXTRA_RATE_KM); + +const getOrderBatch = (o) => { + const t = o.assigntime || o.deliverydate; + if (!t || /^\d{4}-\d{2}-\d{2}$/.test(String(t).trim())) return null; + const d = dayjs(t); + if (!d.isValid()) return null; + const hour = d.hour() + d.minute() / 60; + const batch = BATCHES_DEFAULT.find((b) => hour >= b.startHour && hour < b.endHour); + return batch?.id || null; +}; + +const Profitability = () => { + const [selectedDate, setSelectedDate] = useState(dayjs().format('YYYY-MM-DD')); + + const { + data: deliveriesData, + isLoading, + fetchNextPage, + hasNextPage, + isFetchingNextPage + } = useInfiniteQuery({ + queryKey: ['fetchdeliveries-profitability', 0, 0, 'all', selectedDate, selectedDate, 500, '', 0, 0, 0], + queryFn: fetchDeliveries, + getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined + }); + + const allRows = useMemo(() => (deliveriesData?.pages || []).flatMap((page) => page.rows || []), [deliveriesData]); + + useEffect(() => { + if (hasNextPage && !isFetchingNextPage) fetchNextPage(); + }, [hasNextPage, isFetchingNextPage, fetchNextPage, allRows.length]); + + // Group into { id, riderName, orders } โ€” flat, no zone/suburb bucketing + // (that's a Dispatch-map-specific concern, not needed for a report). + // riderkms is what ProfitabilitySection's revenue/cost math actually reads + // (see orderRevenue/calcRiderMetrics there) but GET /admin/bookings has no + // road-distance field โ€” mapped from the same haversine straight-line `kms` + // every other page in this app already uses as the distance proxy. + const riders = useMemo(() => { + const byRider = new Map(); + allRows.forEach((row) => { + if (!row.userid) return; + const key = String(row.userid); + if (!byRider.has(key)) { + byRider.set(key, { id: key, riderName: row.ridername || `Rider #${key}`, orders: [] }); + } + byRider.get(key).orders.push({ ...row, riderkms: row.kms }); + }); + return Array.from(byRider.values()); + }, [allRows]); + + const totalDailyProfit = useMemo(() => { + let profit = 0; + riders.forEach((rider) => { + let revenue = 0; + let kms = 0; + const batchesWorked = new Set(); + rider.orders.forEach((o) => { + const status = String(o.orderstatus || '').toLowerCase(); + if (SKIPPED_STATUSES.has(status)) return; + const dateStr = o.assigntime + ? dayjs(o.assigntime).format('YYYY-MM-DD') + : o.deliverydate + ? dayjs(o.deliverydate).format('YYYY-MM-DD') + : null; + if (dateStr !== selectedDate) return; + const km = parseFloat(o.riderkms || 0); + revenue += orderRevenue(km); + kms += km; + const batch = getOrderBatch(o); + if (batch) batchesWorked.add(batch); + }); + const variableCost = kms * VARIABLE_RATE_KM; + const fixedCost = Math.min(batchesWorked.size, 3) * FIXED_COST_PER_SLOT; + profit += revenue - variableCost - fixedCost; + }); + return profit; + }, [riders, selectedDate]); + + return ( + <> + {isLoading && } + + + + e.target.value && setSelectedDate(e.target.value)} + InputProps={{ disableUnderline: true }} + sx={{ '& input': { fontWeight: 700, color: DT.textSecondary, p: 0 } }} + /> + + } + /> + + + {!isLoading && riders.length === 0 ? ( + + No deliveries for {dayjs(selectedDate).format('DD MMM YYYY')} + + ) : ( + + )} + + + ); +}; + +export default Profitability; diff --git a/src/pages/nearle/reports/ridersSummary.js b/src/pages/nearle/reports/ridersSummary.js index 9146ef1..9d36667 100644 --- a/src/pages/nearle/reports/ridersSummary.js +++ b/src/pages/nearle/reports/ridersSummary.js @@ -214,15 +214,17 @@ export default function RidersSummary() { }); // Client-side filter across rider name + id. + // GET /admin/milers/summary confirmed live: { milerprofileid, userid, + // displayname, phone, availabilitystatus, rating, assigned, accepted, + // rejected, completed, cancelled, delivered, riderkms, ridercharges, ... } + // โ€” no firstname/lastname/charges/deliveryamt/totalorders/pending/kms/ + // cumulativekms field exists at all, which is why this whole page showed + // "โ€”" everywhere. const filteredRows = useMemo(() => { if (!rows) return []; if (!debouncedSearch) return rows; const q = debouncedSearch.toLowerCase().trim(); - return rows.filter((r) => - [`${r.firstname || ''} ${r.lastname || ''}`, String(r.userid)] - .filter(Boolean) - .some((field) => String(field).toLowerCase().includes(q)) - ); + return rows.filter((r) => [r.displayname, String(r.userid)].filter(Boolean).some((field) => String(field).toLowerCase().includes(q))); }, [rows, debouncedSearch]); // Aggregate KPIs from the loaded summary. @@ -231,9 +233,9 @@ export default function RidersSummary() { return rows.reduce( (acc, r) => { acc.riders += 1; - acc.orders += Number(r.totalorders) || 0; + acc.orders += Number(r.assigned) || 0; acc.delivered += Number(r.delivered) || 0; - acc.amount += Math.max(Number(r.charges) || 0, Number(r.deliveryamt) || 0); + acc.amount += Number(r.ridercharges) || 0; return acc; }, { riders: 0, orders: 0, delivered: 0, amount: 0 } @@ -261,7 +263,7 @@ export default function RidersSummary() { // Total Amount sum (preserved from legacy bottom bar). const total = useMemo(() => { if (!rows) return 0; - return rows.reduce((sum, row) => sum + (Number(row.deliveryamt) || 0), 0); + return rows.reduce((sum, row) => sum + (Number(row.ridercharges) || 0), 0); }, [rows]); const KPI_META = [ @@ -435,8 +437,8 @@ export default function RidersSummary() { ) : ( filteredRows.map((row, index) => { const isOpen = openRow === row.userid; - const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); - const riderName = `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || 'โ€”'; + const amount = Number(row.ridercharges) || 0; + const riderName = row?.displayname || 'โ€”'; return ( { setSelectedRider({ userid: row?.userid, - name: `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || `Rider ${row?.userid}` + name: row?.displayname || `Rider ${row?.userid}` }); setLogDetails(null); setMapOpen(true); @@ -504,13 +506,13 @@ export default function RidersSummary() { > - } /> + } /> } /> - - } /> + + } /> } /> @@ -519,18 +521,10 @@ export default function RidersSummary() { } - label={`${Number(row.kms || 0).toFixed(2)} km`} + label={`${Number(row.riderkms || 0).toFixed(2)} km`} tooltip="KMS" /> - - } - label={`${Number(row.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - # Rider Orders - Pending + Accepted Cancelled Delivered KMS @@ -717,7 +711,7 @@ export default function RidersSummary() { ) : ( filteredRows.map((row, index) => { const isOpen = openRow === row.userid; - const amount = Math.max(Number(row.charges) || 0, Number(row.deliveryamt) || 0); + const amount = Number(row.ridercharges) || 0; return ( - {`${row?.firstname || ''} ${row?.lastname || ''}`.trim() || 'โ€”'} + {row?.displayname || 'โ€”'} ID #{row.userid} @@ -755,10 +749,10 @@ export default function RidersSummary() { - } /> + } /> - } /> + } /> } /> @@ -768,20 +762,12 @@ export default function RidersSummary() { - - } - label={`${Number(row.kms || 0).toFixed(2)} km`} - tooltip="KMS" - /> - } - label={`${Number(row.cumulativekms || 0).toFixed(2)} km`} - tooltip="Actual KMS" - /> - + } + label={`${Number(row.riderkms || 0).toFixed(2)} km`} + tooltip="KMS" + /> @@ -802,7 +788,7 @@ export default function RidersSummary() { onClick={() => { setSelectedRider({ userid: row?.userid, - name: `${row?.firstname || ''} ${row?.lastname || ''}`.trim() || `Rider ${row?.userid}` + name: row?.displayname || `Rider ${row?.userid}` }); setLogDetails(null); setMapOpen(true); @@ -881,7 +867,7 @@ export default function RidersSummary() { variant="caption" sx={{ fontWeight: 800, color: DT.textPrimary, letterSpacing: 0.6, textTransform: 'uppercase' }} > - Tenant Breakdown ยท {`${row?.firstname || ''} ${row?.lastname || ''}`.trim()} + Tenant Breakdown ยท {row?.displayname || ''} diff --git a/src/pages/nearle/requests/requests.js b/src/pages/nearle/requests/requests.js deleted file mode 100644 index 358ea51..0000000 --- a/src/pages/nearle/requests/requests.js +++ /dev/null @@ -1,2032 +0,0 @@ -import { - Grid, - Stack, - Typography, - Box, - Tabs, - Tab, - Chip, - Table, - TableCell, - TableBody, - TableHead, - Avatar, - TableRow, - IconButton, - Collapse, - Divider, - List, - ListItem, - ListItemIcon, - TextField, - Button, - InputAdornment, - FormControl, - OutlinedInput, - Dialog, - DialogContent, - Skeleton, - CircularProgress, - DialogTitle, - FormLabel, - DialogActions, - useMediaQuery, - useTheme, - Paper -} from '@mui/material'; -import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'components/nearle_components/MobileCard'; -import { MdPayments } from 'react-icons/md'; -import { DT, tint } from 'themes/dt/tokens'; - -import { Autocomplete as Autocomplete1 } from '@mui/material'; -import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; -import { SearchOutlined } from '@ant-design/icons'; -import { PopupTransition } from 'components/@extended/Transitions'; -import { enqueueSnackbar } from 'notistack'; -import dayjs from 'dayjs'; -import { PhoneOutlined, MailOutlined } from '@ant-design/icons'; - -import MainCard from 'components/MainCard'; - - -import { useState, useEffect } from 'react'; -import axios from 'axios'; -import Loader from 'components/Loader'; -import Transitions from 'components/@extended/Transitions'; -import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete'; - -import * as React from 'react'; - -import PropTypes from 'prop-types'; -import TableContainer from '@mui/material/TableContainer'; -import TablePagination from '@mui/material/TablePagination'; -import TableSortLabel from '@mui/material/TableSortLabel'; -import { visuallyHidden } from '@mui/utils'; - -const Requests = () => { - // let dispatch = useDispatch(); - - function descendingComparator(a, b, orderBy) { - if (b[orderBy] < a[orderBy]) { - return -1; - } - if (b[orderBy] > a[orderBy]) { - return 1; - } - return 0; - } - - 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]); - } - - const headCells = [ - { - id: 'sno', - disablePadding: true, - label: '#' - }, - { - id: 'tenantname', - numeric: false, - disablePadding: false, - label: 'REQUESTOR' - }, - { - id: 'contact', - numeric: false, - disablePadding: false, - label: 'BANK' - }, - { - id: 'address3', - disablePadding: false, - label: 'IFSC' - }, - { - id: 'address', - disablePadding: false, - label: 'REF NO' - }, - { - id: 'amount', - disablePadding: false, - label: 'AMOUNT' - }, - { - id: 'city', - disablePadding: false, - label: 'REASON' - } - // { - // id: 'action', - // disablePadding: false, - // label: 'ACTION', - // } - ]; - - function EnhancedTableHead(props) { - const { order, orderBy, onRequestSort } = props; - const createSortHandler = (property) => (event) => { - onRequestSort(event, property); - }; - - return ( - - - {headCells.map((headCell) => ( - - - {headCell.label} - {orderBy === headCell.id ? ( - - {order === 'desc' ? 'sorted descending' : 'sorted ascending'} - - ) : null} - - - ))} - - - ); - } - - EnhancedTableHead.propTypes = { - numSelected: PropTypes.number.isRequired, - onRequestSort: PropTypes.func.isRequired, - onSelectAllClick: PropTypes.func.isRequired, - order: PropTypes.oneOf(['asc', 'desc']).isRequired, - orderBy: PropTypes.string.isRequired, - rowCount: PropTypes.number.isRequired - }; - - function EnhancedTable() { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); - const [order, setOrder] = React.useState('asc'); - const [orderBy, setOrderBy] = React.useState('calories'); - const [selected, setSelected] = React.useState([]); - const [page, setPage] = React.useState(0); - const [rowsPerPage, setRowsPerPage] = React.useState(10); - - const [clientname, setClientname] = useState(''); - const [emailaddress, setEmailaddress] = useState(''); - const [mobilenumber, setMobilenumber] = useState(''); - const [regno, setRegno] = useState(''); - const [address, setAddress] = useState(''); - const [city, setCity] = useState(''); - const [zipcode, setZipcode] = useState(''); - const [contactname, setContactname] = useState(''); - const [state1, setState1] = useState(''); - const [suburb, setSuburb] = useState(''); - const [currenttenantid] = useState(''); - const [latlong, setLatlong] = useState({}); - const [alertmessage, setAlertmessage] = useState(''); - // const [toast, setToast] = useState(false); - const [rolesarr, setRolesarr] = useState([]); - const [roleslist] = useState([]); - const [rolestab, setRolestab] = useState(0); - - const [approveid] = useState(false); - const [disableid] = useState(false); - const [loading1] = useState(false); - - const [refno, setRefno] = useState(''); - const [requestor, setRequestor] = useState(''); - const [bankname, setBankname] = useState(''); - const [amount, setAmount] = useState(''); - const [accountno, setAccountno] = useState(''); - const [ifsc, setIfsc] = useState(''); - const [reason, setReason] = useState(''); - const [expandopen, setExpandopen] = useState(''); - const [editexpandopen, setEditexpandopen] = useState(''); - - useEffect(() => { - setRolesarr([ - { - sno: 1, - role: '', - cost: '', - serviceid: 0, - tenantid: 0, - categoryid: 0, - subcategoryid: 0, - servicecode: '', - servicename: '', - unitid: 0, - unitname: '', - serviceamount: '', - discountid: 0, - taxpercent: 0, - taxamount: 0, - servicevalue: 0, - categoryname: '' - } - ]); - console.log(rolesarr); - // fetchroleslist(); - }, []); - - // useEffect(() => { - - // if (alertmessage && toast) { - // dispatch( - // openSnackbar({ - // open: true, - // message: alertmessage, - // variant: 'alert', - // anchorOrigin: { vertical: 'top', horizontal: 'right' }, - // alert: { - // color: 'error', - - // } - // }) - // ) - // } - // }, [toast]) - const opentoast = (message) => { - enqueueSnackbar(message, { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - console.log(alertmessage); - }; - - // const opentoast = () => { - // setToast(true) - - // setTimeout(() => { - // setToast(false) - // }, 2000); - - // } - - const handleAddressPlaceSelected = (place) => { - setAddress(place.formatted_address); - setLatlong({ lat: place.geometry.location.lat(), lng: place.geometry.location.lng() }); - let city1, state, zipcode1, suburb1; - place.address_components.forEach((component) => { - component.types.forEach((type) => { - switch (type) { - case 'locality': - city1 = component.long_name; - break; - case 'administrative_area_level_1': - state = component.long_name; - break; - case 'postal_code': - zipcode1 = component.long_name; - break; - case 'sublocality': - case 'sublocality_level_1': - suburb1 = component.long_name; - break; - } - }); - }); - setCity(city1 || ''); - setState1(state || ''); - setZipcode(zipcode1 || ''); - setSuburb(suburb1 || ''); - }; - - useEffect(() => { - console.log('rolesarr'); - console.log(rolesarr); - }, [rolesarr]); - - - - const addarr = () => { - let arr = rolesarr; - if (arr[arr.length - 1].role && arr[arr.length - 1].cost) { - arr.push({ - sno: arr.length + 1, - cost: '', - role: '', - serviceid: 0, - tenantid: 0, - categoryid: 0, - subcategoryid: 0, - servicecode: '', - servicename: '', - unitid: 0, - unitname: '', - serviceamount: '', - discountid: 0, - taxpercent: 0, - taxamount: 0, - servicevalue: 0, - categoryname: '' - }); - setRolesarr([...arr]); - } else { - // setAlertmessage('Fill all Previous Details'); - opentoast('Fill all Previous Details'); - } - }; - const deletearr = async (sno, val1) => { - console.log(val1); - let arr = rolesarr; - - if (val1.serviceid !== 0 && rolesarr.length > 1) { - console.log([ - { - serviceid: val1.serviceid, - tenantid: val1.tenantid, - categoryid: val1.categoryid, - subcategoryid: val1.subcategoryid, - servicecode: val1.servicecode, - servicename: val1.servicename, - unitid: val1.unitid, - unitname: val1.unitname, - serviceamount: val1.servicevalue - } - ]); - try { - await axios - .delete(`${process.env.REACT_APP_URL2}/tenants/delete/services`, { - data: [ - { - serviceid: val1.serviceid, - tenantid: val1.tenantid, - categoryid: val1.categoryid, - subcategoryid: val1.subcategoryid, - servicecode: val1.servicecode, - servicename: val1.servicename, - unitid: val1.unitid, - unitname: val1.unitname, - serviceamount: val1.servicevalue - } - ] - }) - .then((res) => { - console.log('res'); - console.log(res); - if (res.data.message === 'Deleted successful') { - enqueueSnackbar('Client pricing Deleted', { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - arr.splice(sno - 1, 1); - arr.map((val, i) => { - val.sno = i + 1; - }); - console.log(arr); - setRolesarr([...arr]); - } - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - } catch (err) { - console.log(err); - setLoading(false); - } - } else if (rolesarr.length > 1) { - arr.splice(sno - 1, 1); - arr.map((val, i) => { - val.sno = i + 1; - }); - console.log(arr); - setRolesarr([...arr]); - } else if (rolesarr.length === 1) { - setRolesarr([ - { - sno: 1, - role: '', - cost: '', - serviceid: val1.serviceid, - tenantid: 0, - categoryid: 0, - subcategoryid: 0, - servicecode: '', - servicename: '', - unitid: 0, - unitname: '', - serviceamount: '', - discountid: 0, - taxpercent: 0, - taxamount: 0, - servicevalue: 0, - categoryname: '' - } - ]); - } - }; - - - - - - const clientupdate = async () => { - if (!clientname) { - // setAlertmessage('Fill Business name'); - opentoast('Fill Business name'); - } else if (!regno) { - // setAlertmessage('Fill Business No.'); - opentoast('Fill Business No.'); - } else if (!emailaddress) { - // setAlertmessage('Fill Email address'); - opentoast('Fill Email address'); - } else if (!mobilenumber) { - // setAlertmessage('Fill Mobile number'); - opentoast('Fill Mobile number'); - } else if (!contactname) { - // setAlertmessage('Fill Contact name'); - opentoast('Fill Contact name'); - } else if (!address) { - // setAlertmessage('Fill Address'); - opentoast('Fill Address'); - } else if (!city) { - // setAlertmessage('Fill City name'); - opentoast('Fill City name'); - } else if (!zipcode) { - // setAlertmessage('Fill Zip code'); - opentoast('Fill Zip code'); - } else if (!latlong.lat || !latlong.lng) { - setAlertmessage('Fill correct address'); - opentoast('Fill correct address'); - } else if ((approveid || disableid) && !(rolesarr[0].role && rolesarr[0].cost)) { - opentoast('Fill client pricing'); - } else { - let obj = { - tenantid: currenttenantid, - registrationno: regno, - tenantname: clientname, - primaryemail: emailaddress, - primarycontact: contactname, - contactno: mobilenumber, - address: address, - suburb: suburb, - city: city, - state: state1, - postcode: zipcode, - latitude: latlong.lat.toString(), - longitude: latlong.lng.toString(), - approved: (approveid && tabvalue === 1) || (!approveid && tabvalue === 0 && !disableid) ? 1 : 0 - }; - console.log(obj); - - try { - setLoading(true); - await axios - .put(`${process.env.REACT_APP_URL2}/tenants/update`, obj) - .then((res) => { - console.log('res:', res); - if (res.data.message === 'Update successful') { - // dispatch( - // openSnackbar({ - // open: true, - // message: 'Client Detail Updated Successfully', - // variant: 'alert', - // anchorOrigin: { vertical: 'top', horizontal: 'right' }, - // alert: { - // color: 'success' - // } - // }) - // ) - enqueueSnackbar('Client Details Updated Successfully', { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 3000 - }); - setLoading(true); - - setTimeout(() => { - clientdetailspending(); - clientdetailsapproved(); - setTabvalue(0); - setLoading(false); - }, 2000); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - } catch (err) { - console.log(err); - setLoading(false); - } - } - }; - - const rolepricesubmit = async () => { - console.log('submit'); - let arr = []; - let objcheck = false; - rolesarr.map((val) => { - if (!val.role || !val.cost) { - objcheck = true; - } - arr.push({ - serviceid: val.serviceid, - tenantid: currenttenantid, - categoryid: val.categoryid, - subcategoryid: val.subcategoryid, - servicecode: val.servicecode, - servicename: val.servicename, - unitid: val.unitid, - unitname: val.unitname, - serviceamount: parseFloat(val.serviceamount), - categoryname: val.categoryname, - subcategoryname: val.servicename - // discountid: val.discountid, - // taxpercent: val.taxpercent, - // taxamount: val.taxamount, - // servicevalue: parseFloat(val.servicevalue), - }); - }); - console.log(arr); - if (!objcheck) { - try { - setLoading(true); - // await axios.post(`${process.env.REACT_APP_URL2}/tenants/createservice`, arr)tenants/update/services - await axios - .put(`${process.env.REACT_APP_URL2}/tenants/update/services`, arr) - .then((res) => { - console.log('res:', res); - if (res.data.message === 'Update successful') { - enqueueSnackbar('Service Updated Successfully', { - variant: 'success', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - } - setLoading(false); - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - } catch (err) { - console.log(err); - setLoading(false); - } - } else { - enqueueSnackbar('Fill all Details', { - variant: 'error', - anchorOrigin: { vertical: 'top', horizontal: 'right' }, - autoHideDuration: 2000 - }); - } - }; - - const createrequest = () => { - if (!refno) { - // setAlertmessage('Fill Business name'); - opentoast('Fill Reference No'); - } else if (!requestor) { - // setAlertmessage('Fill Business No.'); - opentoast('Fill Requestor'); - } else if (!bankname) { - // setAlertmessage('Fill Email address'); - opentoast('Fill Bank Name'); - } else if (!amount) { - // setAlertmessage('Fill Mobile number'); - opentoast('Fill Amount'); - } else if (!accountno) { - // setAlertmessage('Fill Contact name'); - opentoast('Fill Account No'); - } else if (!ifsc) { - // setAlertmessage('Fill Address'); - opentoast('Fill IFSC'); - } else if (!reason) { - // setAlertmessage('Fill City name'); - opentoast('Fill Reason'); - } else { - let obj = { - requestid: 0, - requestdate: dayjs().format('YYYY-MM-DD HH:mm:ss'), - referenceno: refno, - Apptypeid: 22, - requesttype: 'staffexpenses', - reason: reason, - requestor: requestor, - amount: amount, - Accountno: accountno, - bankname: bankname, - ifsccode: ifsc - }; - - console.log(obj); - } - }; - - // const rolepriceupdate = async () => { - // console.log('submit') - // let arr = []; - // let objcheck = false; - // rolesarr.map((val) => { - // if (!val.role || !val.cost) { - // objcheck = true; - // } - // arr.push({ - // serviceid: 0, - // tenantid: currenttenantid, - // categoryid: val.categoryid, - // subcategoryid: val.subcategoryid, - // servicecode: val.servicecode, - // servicename: val.servicename, - // unitid: val.unitid, - // unitname: val.unitname, - // serviceamount: parseFloat(val.serviceamount), - // discountid: val.discountid, - // taxpercent: val.taxpercent, - // taxamount: val.taxamount, - // servicevalue: parseFloat(val.servicevalue), - - // }) - // }) - // console.log(arr) - // if (!objcheck) { - // try { - // setLoading(true) - // // await axios.post(`${process.env.REACT_APP_URL2}/clients/createservice`, arr) - // await axios.post(`${process.env.REACT_APP_URL2}/tenants/createservice`, arr) - - // // await axios.post(`${process.env.REACT_APP_URL2}/clients/createservice`, arr) - // .then((res) => { - // console.log('res:', res); - // if (res.data.message === "Successful") { - - // enqueueSnackbar('Service created Successfully', { variant: 'success',anchorOrigin: { vertical: 'top', horizontal: 'right' }, - // autoHideDuration: 3000 }) - // } - // setLoading(false) - // }).catch((err) => { - // console.log(err) - // setLoading(false) - // }) - - // } catch (err) { - // console.log(err); - // setLoading(false) - // } - // } else { - - // enqueueSnackbar('Fill all Details', { variant: 'error',anchorOrigin: { vertical: 'top', horizontal: 'right' }, - // autoHideDuration: 2000 }) - // } - // } - - const handleRequestSort = (event, property) => { - const isAsc = orderBy === property && order === 'asc'; - setOrder(isAsc ? 'desc' : 'asc'); - setOrderBy(property); - }; - - const handleSelectAllClick = (event) => { - if (event.target.checked) { - const newSelected = rows.map((n) => n.name); - setSelected(newSelected); - return; - } - setSelected([]); - }; - - const handleClick = (event, name) => { - const selectedIndex = selected.indexOf(name); - let newSelected = []; - - if (selectedIndex === -1) { - newSelected = newSelected.concat(selected, name); - } else if (selectedIndex === 0) { - newSelected = newSelected.concat(selected.slice(1)); - } else if (selectedIndex === selected.length - 1) { - newSelected = newSelected.concat(selected.slice(0, -1)); - } else if (selectedIndex > 0) { - newSelected = newSelected.concat(selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1)); - } - - setSelected(newSelected); - }; - - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; - - // const handleChangeDense = (event) => { - // setDense(event.target.checked); - // }; - - const isSelected = (name) => selected.indexOf(name) !== -1; - - // Avoid a layout jump when reaching the last page with empty rows. - const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - rows.length) : 0; - - const visibleRows = React.useMemo( - () => stableSort(rows, getComparator(order, orderBy)).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage), - [order, orderBy, page, rowsPerPage] - ); - - return ( - <> - - Create Request - - - - - {/* */} - {/* Client} sx={{ height: '100%' }}> - - */} - - {/* */} - Reference No - { - setRefno(e.target.value); - console.log(e); - }} - /> - - - Requestor - setRequestor(e.target.value)} - // label='Business No.' - fullWidth - sx={{ width: '100%' }} - /> - {/* */} - - - - {/* - */} - - Bank Name - - setBankname(e.target.value)} - // label='Email' - /> - - - Amount - - +1, - // }} - sx={{ width: '100%' }} - onChange={(e) => setAmount(e.target.value)} - /> - {/* - */} - - {/* - - */} - - {/* */} - Account No - - { - setAccountno(e.target.value); - console.log(e); - }} - /> - - - IFSC Code - - setIfsc(e.target.value)} - // label='Business No.' - fullWidth - sx={{ width: '100%' }} - /> - {/* */} - - - Reason - - setReason(e.target.value)} - // label='Business No.' - fullWidth - sx={{ width: '100%' }} - /> - {/* */} - - - - - - - - - - - - - - - - - {isMobile && ( - - {loading && - [0, 1, 2, 3, 4].map((item) => ( - - - - - - - - - - } /> - } /> - - - ))} - - {!loading && - visibleRows.map((row, index) => { - const isItemSelected = isSelected(row.sno); - return ( - handleClick(event, row.sno)} - header={ - - - - {row.requestor ? String(row.requestor).charAt(0).toUpperCase() : '#'} - - - - {row.requestor || 'โ€”'} - - - #{row.sno} - - - - {row.amount != null && ( - - )} - - } - > - - - - - - - - - ); - })} - - {!loading && visibleRows.length === 0 && ( - - - No requests to show - Requests will appear here once available. - - )} - - )} - - - - - - {loading && ( - <> - - {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((item) => ( - - - - - - - - - - - - - - - - - - - - - - - - - - - ))} - - - )} - - - {visibleRows.map((row, index) => { - const isItemSelected = isSelected(row.sno); - const labelId = `enhanced-table-checkbox-${index}`; - return ( - <> - handleClick(event, row.sno)} - role="checkbox" - aria-checked={isItemSelected} - tabIndex={-1} - key={row.sno} - // selected={isItemSelected} - sx={{ cursor: 'pointer' }} - > - {/* - - */} - - {row.sno} - - - - - - - {row.requestor} - {/* - {row.primaryemail} - */} - - - - - {row.accountno} - {row.bankname} - - {row.ifsccode} - - {/* - - {row.address.slice(0, 15)}... - - */} - {row.referenceno} - - {row.amount} - {row.reason} - {/* - - {row.reason} - - */} - - - - - - - - {loading1 ? ( - <> - - {/* - Loading... - */} - - - - - - ) : ( - <> - setRolestab(i)}> - - - - {rolestab === 0 && ( - <> - - - - - - - - - {clientname} - {regno} - - - - {/* - - */} - {/* - - - All orders - - - - - Covered orders - - - - */} - - - - - - - - - - {/* */} - {emailaddress} - {/* */} - - - - - - {/* */} - - {/* - {/* */} - - {/* - - - - - city - - - {/* */} - - - - - - - {/* */} - Contact Details} sx={{ height: '100%' }}> - - - - - - Contact Name - {contactname} - - - - {/* - Contact number - - {mobilenumber} - - */} - - - - - - - Address - {address} - - - - - - - City - {city} - - - - - Zip Code - - {/* */} - {zipcode} - - - - - - - - - {/* */} - - - - )} - {rolestab === 1 && ( - <> - - - - {/* */} - {rolesarr[0].role ? ( - <> - -
- - - # - Category - - Skill - - Cost/Hr - - {/* */} - - - - {rolesarr.map((val1) => { - return ( - <> - - {val1.sno} - {val1.categoryname} - {val1.role} - {val1.cost} - - - ); - })} - -
-
- - ) : ( - <> - No Data found - - )} - {/* - - */} - {/* - - - - - - - - */} - {/* */} - -
- - - )} - - )} - - - - - {/* edit page collapse */} - - - - {loading1 ? ( - <> - - {/* - Loading... - */} - - - - - - ) : ( - <> - - setRolestab(i)}> - - - - {rolestab === 0 && ( - <> - - - - Client} sx={{ height: '100%' }}> - - - {/* */} - {/* */} - - {/* Client name */} - setClientname(e.target.value)} - /> - - setRegno(e.target.value)} - label="Business No." - // placeholder='Registration Number' - fullWidth - sx={{ width: '100%' }} - /> - - {/* */} - - - - - - } - sx={{ width: '100%' }} - onChange={(e) => setEmailaddress(e.target.value)} - label="Email" - /> - - +1 - }} - sx={{ width: '100%' }} - onChange={(e) => setMobilenumber(e.target.value)} - /> - {/* */} - {/* */} - {/* */} - - - - - - - - {/* */} - Contact Details} sx={{ height: '100%' }}> - {/* - */} - - - - {/* Contact Name */} - setContactname(e.target.value)} - label="Contact Name" - /> - {/* name */} - - - - {/* */} - {/* - */} - {/* */} - - - {/* Address */} - {/* {((row.sno === 1)) && */} - - <> - {/* setAddress(e.target.value)} - inputRef={materialRef} - - - /> */} - - - {/* } */} - - - - - {/* */} - {/* - */} - {/* */} - - - {/* City */} - {/* City */} - setCity(e.target.value)} - /> - {/* {'data.address'} */} - - - - - {/* Zip Code */} - {/* - - */} - setZipcode(e.target.value)} - /> - - - - - - - - - {/* - - */} - - - {/* */} - - - - - )} - {rolestab === 1 && ( - <> - - - - - - - - - # - Category - - Skill - - Cost/Hr - - - - - - {rolesarr.map((val1) => { - return ( - <> - - {val1.sno} - {/* {val1.sno} */} - {val1.categoryname} - - - -b.categoryname.localeCompare(a.categoryname) - )} - groupBy={(option) => option.categoryname} - getOptionLabel={(option) => option.subcategoryname} - isOptionEqualToValue={(option, value) => - option.subcategoryid === value.subcategoryid - } - onChange={(e, val) => { - if (val) { - console.log('eval', e); - // console.log(skillsarr) - // let res = rolesarr.find((val2) => val2.role === val.servicename); - let res = rolesarr.find( - (val2) => val2.subcategoryid === val.subcategoryid - ); - - console.log(val); - if (!res) { - let arr = rolesarr; - arr[val1.sno - 1].role = val.subcategoryname; - arr[val1.sno - 1].categoryname = val.categoryname; - - // arr[val1.sno - 1].Staffroleid = 0; - - arr[val1.sno - 1].categoryid = val.categoryid; - arr[val1.sno - 1].subcategoryid = val.subcategoryid; - // arr[val1.sno - 1].servicename = val.servicename; - arr[val1.sno - 1].servicename = val.subcategoryname; - // arr[val1.sno - 1].servicecode = val.servicecode; - - // arr[val1.sno - 1].unitid = val.serviceunit; - arr[val1.sno - 1].unitid = val.unitid; - // arr[val1.sno - 1].serviceid = val.serviceid; - - // arr[val1.sno - 1].unitname = val.serviceunitname; - arr[val1.sno - 1].unitname = val.unitname; - - // setSkillsarr([...arr]); - setRolesarr([...arr]); - } else { - // setAlertmessage('select different skill') - opentoast('select different skill'); - } - } - console.log(val); - }} - renderInput={(params) => ( - - )} - // renderGroup={(params) => ( - //
  • - - //
    {params.group}
    - //

    {params.children}

    - - //
  • - // )} - // options={roleslist} - value={{ - categoryid: val1.categoryid, - categoryname: '', - cost: val1.serviceamount, - label: val1.servicename, - status: 0, - subcategoryid: val1.subcategoryid, - subcategoryname: val1.servicename, - unitid: val1.unitid, - unitname: val1.unitname - }} - // textContent={val1.servicename} - - disabled={loading ? true : false} - /> -
    - - - { - let arr = rolesarr; - if (e.target.value < 1000) { - arr[val1.sno - 1].cost = e.target.value; - arr[val1.sno - 1].servicevalue = e.target.value; - arr[val1.sno - 1].serviceamount = e.target.value; - - setRolesarr([...arr]); - } - - // forceUpdate() - console.log(e.target.value); - }} - value={val1.cost} - autoComplete="off" - fullWidth - /> - - - - deletearr(val1.sno, val1)} color="error"> - - - -
    - - ); - })} -
    -
    -
    - - - - - - - {/* */} - - - - {/* */} - -
    -
    -
    -
    - - )} -
    - - )} -
    -
    - - - - ); - })} - {emptyRows > 0 && ( - - - - )} - - - - - {/* */} - {/* } - label="Dense padding" - /> */} - {/* */} - - - ); - } - - const outerTheme = useTheme(); - const isMobile = useMediaQuery(outerTheme.breakpoints.down('md')); - const [tabvalue, setTabvalue] = useState(0); - const [rows, setRows] = useState([]); - const [clientapproved, setClientApproved] = useState([]); - const [clientpending, setClientPending] = useState([]); - const [loading, setLoading] = useState(false); - const [searchword, setSearchword] = useState(''); - const [dialogopen, setDialogopen] = useState(false); - - // const setinitial = (val)=>{ - // if(val){ - - // console.log(val); - // setClientname(val.tenantname) - // }else{ - // setClientname('') - // } - // console.log(clientname) - - // } - - useEffect(() => { - if (localStorage.getItem('partnerid')) { - clientdetailspending(localStorage.getItem('partnerid')); - clientdetailsapproved(localStorage.getItem('partnerid')); - } - }, []); - - useEffect(() => { - // if (searchword) { - // if (tabvalue === 0) { - // let arr = clientapproved.filter((val) => { - // return (val.tenantname.toLowerCase().includes(searchword.toLowerCase()) - // || val.primarycontact.toLowerCase().includes(searchword.toLowerCase()) - // || val.primaryemail.toLowerCase().includes(searchword.toLowerCase()) - // || val.city.toString().toLowerCase().includes(searchword.toLowerCase()) - // ) - // }) - // console.log(arr) - // setRows([...arr]) - // } - // if (tabvalue === 1) { - // let arr = clientpending.filter((val) => { - // return (val.tenantname.toLowerCase().includes(searchword.toLowerCase()) - // || val.primarycontact.toLowerCase().includes(searchword.toLowerCase()) - // || val.primaryemail.toLowerCase().includes(searchword.toLowerCase()) - // || val.city.toString().toLowerCase().includes(searchword.toLowerCase()) - // ) - // }) - // console.log(arr) - // setRows([...arr]) - // } - // } - }, [searchword, tabvalue]); - - const handleChangetab = (e, i) => { - setTabvalue(i); - if (i === 1) setRows(clientapproved); - if (i === 0) setRows(clientpending); - }; - - const clientdetailsapproved = async (tid) => { - setLoading(true); - try { - await axios - .get(`${process.env.REACT_APP_URL}/payments/requests/getpaymentrequest/?partnerid=${tid}&status=1`) - - .then((res) => { - if (res.data.message === 'Successful') { - let arr = []; - res.data.details.map((val, i) => { - arr = [...arr, { ...val, sno: i + 1 }]; - }); - // setArr(arr) - setClientApproved(arr); - console.log(res.data.details); - setLoading(false); - } - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - } catch (err) { - console.log(err); - setLoading(false); - } - }; - - const clientdetailspending = async (tid) => { - setLoading(true); - try { - await axios - .get(`${process.env.REACT_APP_URL}/payments/requests/getpaymentrequest/?partnerid=${tid}&status=0`) - - .then((res) => { - if (res.data.message === 'Success') { - let arr = []; - res.data.details.map((val, i) => { - arr = [...arr, { ...val, sno: i + 1 }]; - }); - // setArr(arr) - setClientPending(arr); - setRows(arr); - console.log(res.data.details); - setLoading(false); - } - }) - .catch((err) => { - console.log(err); - setLoading(false); - }); - } catch (err) { - console.log(err); - setLoading(false); - } - }; - - const dialogclose = () => { - setDialogopen(false); - }; - - return ( - <> - {loading && } - - - - - - - - - - Payment Requests - - - - - - - - {/* */} - - - - } - iconPosition="end" - /> - } - icon={} - /> - - - - - - - } - aria-describedby="header-search-text" - inputProps={{ - 'aria-label': 'weight' - }} - placeholder="Search" - value={searchword} - onChange={(e) => { - setSearchword(e.target.value); - }} - autoComplete="off" - /> - - - {/* */} - - - - - - ); -}; - -export default Requests; diff --git a/src/pages/nearle/riders/RiderSubstitution.js b/src/pages/nearle/riders/RiderSubstitution.js index 449631f..8481748 100644 --- a/src/pages/nearle/riders/RiderSubstitution.js +++ b/src/pages/nearle/riders/RiderSubstitution.js @@ -114,9 +114,27 @@ export default function RiderSubstitution({ [selectedDate] ); + // Buckets match the same morning/afternoon/evening hour ranges used + // elsewhere in the app (e.g. Dispatch's batch model). row.starttime's + // real format isn't confirmed against live data yet (this row source is + // currently a documented no-backend stub โ€” see riders.js), but this is + // still correct once real rows exist, rather than the toggle silently + // doing nothing regardless of what's selected. + const getShiftBatch = (starttime) => { + if (!starttime) return null; + const d = dayjs(starttime, ['HH:mm:ss', 'HH:mm', 'YYYY-MM-DD HH:mm:ss']); + if (!d.isValid()) return null; + const hour = d.hour() + d.minute() / 60; + if (hour >= 0 && hour < 8) return 'morning'; + if (hour >= 9 && hour < 12.5) return 'afternoon'; + if (hour >= 16 && hour < 19) return 'evening'; + return null; + }; + const filteredRows = React.useMemo(() => { - return rows; - }, [rows]); + if (selectedBatch === 'all') return rows; + return (rows || []).filter((row) => getShiftBatch(row.starttime) === selectedBatch); + }, [rows, selectedBatch]); const hasAssignments = Object.values(substituteAssignments || {}).some((val) => val !== null && val !== undefined); diff --git a/src/pages/nearle/riders/editRider.js b/src/pages/nearle/riders/editRider.js index 413ae17..49a07bd 100644 --- a/src/pages/nearle/riders/editRider.js +++ b/src/pages/nearle/riders/editRider.js @@ -22,20 +22,12 @@ import { MdDirectionsBike } from 'react-icons/md'; import { DT, tint } from 'themes/dt/tokens'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -// third-party -// import { PatternFormat } from 'react-number-format'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; - // project import import MainCard from 'components/MainCard'; import { getMiler, updateMiler, getAdminTenant } from 'pages/api/doormileApi'; import { fetchAppLocations as fetchZoneLocations } from 'pages/api/api'; -import AddressAutocomplete from 'components/nearle_components/AddressAutocomplete'; import Loader from 'components/Loader'; import { enqueueSnackbar } from 'notistack'; -import dayjs from 'dayjs'; import CircularLoader from 'components/CircularLoader'; const EditRider = () => { @@ -44,17 +36,8 @@ const EditRider = () => { const location = useLocation(); const [riderdata, setRiderdata] = useState(null); const navigate = useNavigate(); - const [address, setAddress] = useState(''); - const [suburb, setSuburb] = useState(''); - const [city, setCity] = useState(''); - const [state, setState] = useState(''); - const [partner, setPartner] = useState({}); - const [vehiclelist, setVehiclelist] = useState([]); - const [accountlist, setAccountlist] = useState([]); const [partnerlist, setPartnerlist] = useState([]); - - const [shiftlist, setShiftlist] = useState([]); const [locaName, setLocoName] = useState(); const [loading, setLoading] = useState(false); @@ -63,9 +46,8 @@ const EditRider = () => { try { const miler = await getMiler(id); setRiderdata(miler); - fetchridershifts(miler?.applocationid); } catch (error) { - console.log('fetchmanagerList', error); + // swallow } }; useEffect(() => { @@ -75,81 +57,28 @@ const EditRider = () => { fetchRiderData(location.state.riderdata.milerprofileid); }, []); - useEffect(() => { - console.log('riderdata', riderdata); - }, [riderdata, address]); - - useEffect(() => { - setAddress(location.state.riderdata.address); - setSuburb(location.state.riderdata.suburb); - setCity(location.state.riderdata.city); - setState(location.state.riderdata.state); - }, []); - useEffect(() => { if (localStorage.getItem('tenantid')) { fetchtenantinfo(localStorage.getItem('tenantid')); } - fetchvehicle(); - fetchaccounttype(); }, []); - useEffect(() => { - if (partner.applocationid) { - fetchridershifts(partner.applocationid); - } - }, [partner.applocationid]); - const fetchtenantinfo = async (tid) => { setLoading(true); try { await getAdminTenant(tid); } catch (err) { - console.log(err); + // swallow } finally { setLoading(false); } }; - // No vehicle-type/account-type lookup endpoint in the new API โ€” these lists - // stay empty (dropdowns render with no options) instead of crashing. - const fetchvehicle = async () => setVehiclelist([]); - - const fetchaccounttype = async () => setAccountlist([]); - // ==============================|| fetchAppLocations ||============================== // useEffect(() => { fetchZoneLocations().then(setPartnerlist); }, []); - // No rider-shift lookup endpoint in the new API. - const fetchridershifts = async () => setShiftlist([]); - - const handleAddressPlaceSelected = (place) => { - setAddress(place.formatted_address); - - let city1, state1, suburb1; - for (let i = 0; i < place.address_components.length; i++) { - for (let j = 0; j < place.address_components[i].types.length; j++) { - switch (place.address_components[i].types[j]) { - case 'locality': - city1 = place.address_components[i].long_name; - break; - case 'administrative_area_level_1': - state1 = place.address_components[i].long_name; - break; - case 'sublocality': - case 'sublocality_level_1': - suburb1 = place.address_components[i].long_name; - break; - } - } - } - setCity(city1 || ''); - setState(state1 || ''); - setSuburb(suburb1 || ''); - }; - const updateRider = async () => { setLoading(true); // PUT /admin/milers/:id has no documented fields for bank/vehicle/shift @@ -335,84 +264,9 @@ const EditRider = () => { /> - {/* ========================== ||Address || ========================== */} - - - Address - - - - {/* ========================== || Location|| ========================== */} - - - Location - { - setRiderdata({ - ...riderdata, - suburb: e.target.value - }); - setSuburb(e.target.value); - }} - value={suburb} - autoComplete="off" - /> - - - {/* ========================== || City|| ========================== */} - - - City - { - setRiderdata({ - ...riderdata, - city: e.target.value - }); - setCity(e.target.value); - }} - value={city} - autoComplete="off" - /> - - - {/* ========================== || State|| ========================== */} - - - State - { - setRiderdata({ - ...riderdata, - state: e.target.value - }); - setState(e.target.value); - }} - value={state} - autoComplete="off" - /> - - + {/* Address/Suburb/City/State removed โ€” GET /admin/milers has + no such fields at all (confirmed live), so they always + loaded blank and updateRider() never submitted them. */} {/* ========================== || Identification No|| ========================== */} @@ -445,459 +299,25 @@ const EditRider = () => { onChange={(event, value) => { if (value) { // This picker is fetchAppLocations() (hub-derived - // cities), not a Partners resource โ€” the field used - // to write a `partnerid` that doesn't exist on - // these options (always undefined) alongside the - // real applocationid. Only applocationid is a - // confirmed miler field. + // cities), not a Partners resource โ€” applocationid + // is the only confirmed miler field it feeds. setLocoName(value.locationname); - fetchridershifts(value.applocationid); setRiderdata({ ...riderdata, applocationid: value.applocationid }); - } else { - setPartner({}); } }} renderInput={(params) => } /> - {/* } - label={locaName} - options={partnerlist} - getOptionLabel={(option) => `${option.locationname}`} - onChange={(e, val) => { - if (val) { - setPartner(val); - setRiderdata({ - ...riderdata, - partnerid: val.partnerid - }); - } else { - setPartner({}); - } - }} - freeSolo - /> */} - - - - - {/* || =========================================== || Charges || =========================================== || */} - - - Charges - - } - > - - {/* ========================== || Shift Type || ========================== */} - - - - Shift Type - - ( - - )} - // disabled - options={shiftlist} - onChange={(e, val) => { - if (val) { - console.log('shift', val); - setRiderdata({ - ...riderdata, - shiftid: val.shiftid, - additionalkm: val.additionalkm, - additionalcharges: val.additionalcharges, - basefare: val.basefare, - starttime: val.starttime, - endtime: val.endtime - }); - } - }} - freeSolo - /> - - - {/* ========================== || Base Fare || ========================== */} - - - - Base Fare - { - setRiderdata({ - ...riderdata, - basefare: e.target.value - }); - }} - value={riderdata?.basefare} - autoComplete="off" - disabled - /> - - - {/* ========================== || Additional Kms || ========================== */} - - - - Additional Kms - { - setRiderdata({ - ...riderdata, - additionalkm: e.target.value - }); - }} - value={riderdata?.additionalkm} - autoComplete="off" - disabled - /> - - - {/* ========================== || Other Charges || ========================== */} - - - - Other Charges - { - setRiderdata({ - ...riderdata, - additionalcharges: e.target.value - }); - }} - value={riderdata?.additionalcharges} - autoComplete="off" - disabled - /> - - - - - - {/* || =========================================== || Bank Details || =========================================== || */} - - - Bank Details - - } - > - - {' '} - {/* ========================== || Account No|| ========================== */} - - - Account No - { - setRiderdata({ - ...riderdata, - accountno: e.target.value - }); - }} - autoComplete="off" - /> - - {' '} - {/* ========================== || Account Name || ========================== */} - - - Account Name - { - setRiderdata({ - ...riderdata, - accountname: e.target.value - }); - }} - autoComplete="off" - /> - - {' '} - {/* ========================== || Account Type || ========================== */} - - - Account Type - - ( - - )} - // disabled - options={accountlist} - // value={clientdetail} - onChange={(e, val) => { - console.log('ac type', val); - if (val) { - setRiderdata({ - ...riderdata, - accounttype: val.label - }); - // fetchroles(val.tenantid); - } - }} - freeSolo - /> - - {' '} - {/* ========================== || Bank Name|| ========================== */} - - - Bank Name - { - setRiderdata({ - ...riderdata, - bankname: e.target.value - }); - }} - autoComplete="off" - /> - - {' '} - {/* ========================== || IFSC Code || ========================== */} - - - IFSC Code - { - setRiderdata({ - ...riderdata, - ifsccode: e.target.value - }); - }} - autoComplete="off" - /> - - - {/* ========================== || Branch || ========================== */} - - - Branch - { - setRiderdata({ - ...riderdata, - branch: e.target.value - }); - }} - autoComplete="off" - /> - - - - - - {/* || =========================================== || Vehicle Details || =========================================== || */} - - - Vehicle Details - - } - > - - {/* ========================== || Vehicle Name || ========================== */} - - - Vehicle Name - ( - - )} - // disabled - options={vehiclelist} - // value={clientdetail} - onChange={(e, val) => { - if (val) { - console.log('vehi', val); - setRiderdata({ - ...riderdata, - vehiclename: val.label, - vehicleid: val.apptypeid - }); - - // fetchroles(val.tenantid); - } - }} - freeSolo - /> - - {' '} - {/* ========================== || Vehicle No || ========================== */} - - - Vehicle No - { - setRiderdata({ - ...riderdata, - vehicleno: e.target.value - }); - }} - value={riderdata?.vehicleno} - autoComplete="off" - /> - - {' '} - {/* ========================== || Model Year || ========================== */} - - - Model Year - { - setRiderdata({ - ...riderdata, - model: e.target.value - }); - }} - value={riderdata?.model} - autoComplete="off" - /> - - {' '} - {/* ========================== || Vehicle Color || ========================== */} - - - Vehicle Color - { - setRiderdata({ - ...riderdata, - color: e.target.value - }); - }} - value={riderdata?.color} - autoComplete="off" - /> - - {' '} - {/* ========================== || License No || ========================== */} - - - License No - { - setRiderdata({ - ...riderdata, - licenseno: e.target.value - }); - }} - value={riderdata?.licenseno} - autoComplete="off" - /> - - {' '} - {/* ========================== || Insurance No || ========================== */} - - - Insurance No - { - setRiderdata({ - ...riderdata, - insuranceno: e.target.value - }); - }} - value={riderdata?.insuranceno} - autoComplete="off" - /> - - {' '} - {/* ========================== || Insurance Expiry Date || ========================== */} - - - Insurance Expiry Date - - { - setRiderdata({ - ...riderdata, - insurancedate: dayjs(e.$d).format('YYYY-MM-DD 00:00:00') - }); - }} - sx={{ width: '100%' }} - // disablePast - // minDate={dayjs().add(1, 'day')} - /> - + {/* Charges/Bank Details/Vehicle Details sections removed โ€” PUT + /admin/milers/:id has no matching fields for any of them (no + shift/basefare/bank/vehicle sub-object on a miler), so nothing + typed into them ever reached the server. */} {/* ================= FIXED BOTTOM ACTION ================= */} diff --git a/src/pages/nearle/riders/riders.js b/src/pages/nearle/riders/riders.js index c9205e4..a580ac6 100644 --- a/src/pages/nearle/riders/riders.js +++ b/src/pages/nearle/riders/riders.js @@ -68,7 +68,7 @@ import { MobileCard, MobileCardList, MobileField, MobileFieldGrid } from 'compon import DebounceSearchBar from 'components/nearle_components/DebounceSearchBar'; import CircularLoader from 'components/CircularLoader'; import { fetchAllRiders, getallridersummary } from 'pages/api/api'; -import { getMilers, blockMiler, assignMilerVehicle, getVehicles } from 'pages/api/doormileApi'; +import { getMilers, blockMiler, assignMilerVehicle, getVehicles, getMilerLogs, getMiler } from 'pages/api/doormileApi'; import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import LoaderWithImage from 'components/nearle_components/LoaderWithImage'; import { OrdersTableSkeleton } from '../orders/OrdersTableSkeleton'; @@ -144,6 +144,10 @@ const STATUS_META = { available: { label: 'Available', color: '#10b981', icon: MdCheckCircle }, assigned: { label: 'Assigned', color: '#6366f1', icon: MdTwoWheeler }, on_pickup: { label: 'On Pickup', color: '#14b8a6', icon: MdTwoWheeler }, + // Confirmed live: PUT /admin/milers/:id/block sets availabilitystatus to + // "Blocked" โ€” a value this map didn't have, so a blocked rider fell + // through to "unknown" instead of clearly reading as blocked. + blocked: { label: 'Blocked', color: '#7f1d1d', icon: MdBlock }, idle: { label: 'Idle', color: '#f59e0b', icon: MdAccessTime }, unknown: { label: 'Unknown', color: '#94a3b8', icon: MdInventory2 } }; @@ -178,7 +182,7 @@ const Riders = () => { const roleid = localStorage.getItem('roleid'); const totalCols = roleid == 1 ? 11 : 10; const [logsRow, setLogsRow] = useState(null); - const [riderLogsdata] = useState(null); + const [riderLogsdata, setRiderLogsdata] = useState(null); const [historyDate, setHistoryDate] = useState(dayjs()); const [historyStatus, setHistoryStatus] = useState('all'); const [selectedDate, setSelectedDate] = useState(dayjs()); @@ -281,10 +285,37 @@ const Riders = () => { }); // ==============================|| getRiderLogs (riders)||============================== // - // No live GPS/battery/periodic-log endpoint in the new API โ€” the expanded - // row still opens, showing the panel's built-in "โ€”" placeholders. - const getRiderLogs = async () => { - OpenToast('Live rider logs are not available on the Doormile Express API yet.', 'warning', 2500); + // GET /admin/milers/:id/logs is confirmed live (Status: Done) โ€” id is + // milerprofileid, not userid. A log entry's own field names are still + // unconfirmed (every rider in the tenant we tested against had an empty + // log history), so this falls back to the miler's own last-known position + // (currentlatitude/currentlongitude/lastlocationupdatedat/ + // availabilitystatus โ€” all confirmed fields) when there's no ping yet, + // rather than showing nothing. + const getRiderLogs = async (milerprofileid) => { + if (!milerprofileid) return; + setRiderLogsdata(null); + try { + const logs = await getMilerLogs(milerprofileid, undefined, undefined, 1); + const latest = Array.isArray(logs) ? logs[0] : null; + if (latest) { + setRiderLogsdata(latest); + } else { + const miler = await getMiler(milerprofileid); + setRiderLogsdata( + miler + ? { + latitude: miler.currentlatitude, + longitude: miler.currentlongitude, + logdate: miler.lastlocationupdatedat || miler.lastpingat, + status: miler.availabilitystatus + } + : null + ); + } + } catch (err) { + OpenToast('Could not load rider logs', 'error', 2000); + } }; // ==============================|| fetchAllRiders||============================== // @@ -312,8 +343,10 @@ const Riders = () => { // no-ops until filtered client-side here. const filteredRows = rows.filter((row) => { // "Active" here means currently online/working (Available/Assigned/ - // On_Pickup) โ€” the only real "not working" state is Offline. - if (tabvalue === 1 && String(row.availabilitystatus || '').toLowerCase() === 'offline') return false; + // On_Pickup) โ€” Offline and Blocked (confirmed live enum) both mean not + // working. + const status = String(row.availabilitystatus || '').toLowerCase(); + if (tabvalue === 1 && (status === 'offline' || status === 'blocked')) return false; if (!debouncedSearch) return true; const q = debouncedSearch.toLowerCase(); return [row.displayname, row.phone].some((v) => String(v || '').toLowerCase().includes(q)); @@ -992,7 +1025,7 @@ const Riders = () => { filteredRows?.map((row, index) => { const statusMeta = getRowStatusMeta(row); const StatusIcon = statusMeta.icon; - const expanded = logsRow === row.userid; + const expanded = logsRow === row.milerprofileid; return ( { '&:hover': { bgcolor: '#0ea5e9', color: '#fff' } }} onClick={() => { - if (row.userid == logsRow) { + if (row.milerprofileid == logsRow) { setLogsRow(null); } else { - setLogsRow(row.userid); - getRiderLogs(row.userid); + setLogsRow(row.milerprofileid); + getRiderLogs(row.milerprofileid); } }} > @@ -1343,7 +1376,7 @@ const Riders = () => { filteredRows?.map((row, index) => { const statusMeta = getRowStatusMeta(row); const StatusIcon = statusMeta.icon; - const expanded = logsRow === row.userid; + const expanded = logsRow === row.milerprofileid; return ( { '&:hover': { bgcolor: '#0ea5e9', color: '#fff' } }} onClick={() => { - if (row.userid == logsRow) { + if (row.milerprofileid == logsRow) { setLogsRow(null); } else { - setLogsRow(row.userid); - getRiderLogs(row.userid); + setLogsRow(row.milerprofileid); + getRiderLogs(row.milerprofileid); } }} > diff --git a/src/pages/nearle/vehicles/vehicles.js b/src/pages/nearle/vehicles/vehicles.js index 80d61ca..357e396 100644 --- a/src/pages/nearle/vehicles/vehicles.js +++ b/src/pages/nearle/vehicles/vehicles.js @@ -64,7 +64,12 @@ const emptyForm = { maxvolume: '', partner: null, batterypercentage: '', - status: 'active' + // Confirmed live: POST /admin/vehicles rejects "active"/"Active"/ + // "inactive"/"Inactive"/"Unavailable"/"Maintenance"/"Offline" โ€” the only + // value that worked was "Available". Status is a required field with a + // narrow, still not fully mapped enum, so this only offers the one + // confirmed-working value rather than guessing a second one that fails. + status: 'Available' }; const Vehicles = () => { @@ -88,7 +93,10 @@ const Vehicles = () => { const stats = useMemo(() => { const total = vehicles.length; - const active = vehicles.filter((v) => String(v.status).toLowerCase() === 'active').length; + // Only 'Available' is a confirmed-valid status on this resource (see the + // comment on emptyForm.status above) โ€” no vehicle can ever have a status + // of 'active', so counting against that string always read 0. + const active = vehicles.filter((v) => String(v.status).toLowerCase() === 'available').length; const lowBattery = vehicles.filter((v) => Number(v.batterypercentage) > 0 && Number(v.batterypercentage) < 20).length; return { total, active, lowBattery }; }, [vehicles]); @@ -137,7 +145,7 @@ const Vehicles = () => { maxvolume: row.maxvolume ?? '', partner: partnerMap.get(row.partnerid) || null, batterypercentage: row.batterypercentage ?? '', - status: row.status || 'active' + status: row.status || 'Available' }); setDialogOpen(true); }; @@ -184,7 +192,7 @@ const Vehicles = () => { } color={BRAND} /> - } color="#10b981" /> + } color="#10b981" /> } color="#f59e0b" /> @@ -323,10 +331,12 @@ const Vehicles = () => { Status - + {/* Only "Available" is confirmed to work against POST + /admin/vehicles (Active/Inactive/Unavailable/Maintenance/ + Offline all rejected live) โ€” not offering a second option + that's known to fail. On edit this preserves whatever the + vehicle's real status already is. */} + diff --git a/src/routes/MainRoutes.js b/src/routes/MainRoutes.js index c4b309f..966f6d4 100644 --- a/src/routes/MainRoutes.js +++ b/src/routes/MainRoutes.js @@ -30,10 +30,12 @@ const Createorder1 = Loadable(lazy(() => import('pages/nearle/orders/createorder const MultipleOrders = Loadable(lazy(() => import('pages/nearle/orders/multipleOrders'))); const Createclient = Loadable(lazy(() => import('pages/nearle/clients/createclient'))); +const CreateCustomer = Loadable(lazy(() => import('pages/nearle/clients/createCustomer'))); const OrdersSummary = Loadable(lazy(() => import('pages/nearle/reports/ordersSummary'))); const OrdersDetails = Loadable(lazy(() => import('pages/nearle/reports/ordersDetails'))); const RidersSummary = Loadable(lazy(() => import('pages/nearle/reports/ridersSummary'))); +const Profitability = Loadable(lazy(() => import('pages/nearle/reports/profitability'))); const Riders = Loadable(lazy(() => import('pages/nearle/riders/riders'))); const Createrider = Loadable(lazy(() => import('pages/nearle/riders/createrider'))); @@ -124,6 +126,10 @@ const MainRoutes = { { path: 'riderssummary', element: + }, + { + path: 'profitability', + element: } ] }, @@ -139,6 +145,10 @@ const MainRoutes = { path: 'customers', element: }, + { + path: 'customer/create', + element: + }, { path: 'hubs', element: diff --git a/src/store/reducers/actions.js b/src/store/reducers/actions.js deleted file mode 100644 index d5a0eb8..0000000 --- a/src/store/reducers/actions.js +++ /dev/null @@ -1,4 +0,0 @@ -// action - account reducer -export const LOGIN = '@auth/LOGIN'; -export const LOGOUT = '@auth/LOGOUT'; -export const REGISTER = '@auth/REGISTER'; diff --git a/src/store/reducers/auth.js b/src/store/reducers/auth.js deleted file mode 100644 index c9ebee3..0000000 --- a/src/store/reducers/auth.js +++ /dev/null @@ -1,48 +0,0 @@ -// action - state management -import { REGISTER, LOGIN, LOGOUT } from './actions'; - -// initial state -export const initialState = { - // isLoggedIn: false, - // isInitialized: false, - // user: null - isLoggedIn: true, - isInitialized: true, - user: null -}; - -// ==============================|| AUTH REDUCER ||============================== // - -const auth = (state = initialState, action) => { - switch (action.type) { - case REGISTER: { - const { user } = action.payload; - return { - ...state, - user - }; - } - case LOGIN: { - const { user } = action.payload; - return { - ...state, - isLoggedIn: true, - isInitialized: true, - user - }; - } - case LOGOUT: { - return { - ...state, - isInitialized: true, - isLoggedIn: false, - user: null - }; - } - default: { - return { ...state }; - } - } -}; - -export default auth; diff --git a/src/store/reducers/loginUserSlice.js b/src/store/reducers/loginUserSlice.js index fe97720..220a2cf 100644 --- a/src/store/reducers/loginUserSlice.js +++ b/src/store/reducers/loginUserSlice.js @@ -14,18 +14,12 @@ const loginUserSlice = createSlice({ state.user = action.payload; }, - updateUserFcmToken(state, action) { - if (state.user) { - state.user.userfcmtoken = action.payload; - } - }, - logoutUser() { return initialState; // ๐Ÿ”ฅ clears entire slice } } }); -export const { setLoginUser, updateUserFcmToken, logoutUser } = loginUserSlice.actions; +export const { setLoginUser, logoutUser } = loginUserSlice.actions; export default loginUserSlice.reducer; diff --git a/src/store/reducers/menu.js b/src/store/reducers/menu.js index 589c3d4..ce1af93 100644 --- a/src/store/reducers/menu.js +++ b/src/store/reducers/menu.js @@ -1,8 +1,5 @@ // third-party -import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; - -// project import -import axios from 'utils/axios'; +import { createSlice } from '@reduxjs/toolkit'; // initial state const initialState = { @@ -18,11 +15,6 @@ const initialState = { // ==============================|| SLICE - MENU ||============================== // -export const fetchMenu = createAsyncThunk('', async () => { - const response = await axios.get('/api/menu/dashboard'); - return response.data; -}); - const menu = createSlice({ name: 'menu', initialState, @@ -53,12 +45,6 @@ const menu = createSlice({ setSelectedMenu(state, action) { state.selectedMenu = action.payload; } - }, - - extraReducers(builder) { - builder.addCase(fetchMenu.fulfilled, (state, action) => { - state.menu = action.payload.dashboard; - }); } }); diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json index 67a4805..7497109 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -14,6 +14,7 @@ "ordersummary": "Orders Summary", "ordersdetails": "Orders Details", "riderssummary": "Riders Summary", + "profitability": "Profitability", "dispatch": "Dispatch", "customers": "Customers", "fleetops": "Fleet & Ops", diff --git a/src/utils/logger.js b/src/utils/logger.js index 549143b..1fc86cf 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -15,11 +15,6 @@ const style = (bg, color) => `background: ${bg}; color: ${color}; padding: 2px 5 const PREFIX = '%c[Doormile Express]'; 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; @@ -30,10 +25,17 @@ const print = (levelName, args, labelStyle) => { const formatPrefix = `${PREFIX}%c ${levelName}`; const styles = [PREFIX_STYLE, labelStyle]; + // Read console.log/warn/error fresh on every call rather than capturing + // them at module-import time โ€” index.js's production console-disable + // override (console.log = () => {}, etc.) runs as part of its own + // top-level code, which executes AFTER this module's imports are + // evaluated. Capturing eagerly grabbed the real methods before that + // override existed, so logger.warn/error silently bypassed the prod + // safety net and kept printing to the browser console. const consoleMethod = - levelName === 'ERROR' ? originalError : - levelName === 'WARN' ? originalWarn : - originalLog; + levelName === 'ERROR' ? console.error || console.log : + levelName === 'WARN' ? console.warn || console.log : + console.log; if (isMessageString) { consoleMethod(