updates on the create order page ui changes

This commit is contained in:
2026-05-27 11:07:26 +05:30
parent 122dd220be
commit 8c2248974e
9 changed files with 6083 additions and 3288 deletions

View File

@@ -1,3 +1,4 @@
import logger from './utils/logger';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
@@ -38,6 +39,8 @@ if (process.env.NODE_ENV !== 'development') {
console.warn = () => {}; // Optionally disable console.warn
}
logger.info('NearlExpress console application starting...');
// const root = ReactDOM.createRoot(document.getElementById('root'));
// ==============================|| MAIN - REACT DOM RENDER ||============================== //

View File

@@ -1,4 +1,5 @@
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { Outlet } from 'react-router-dom';
// material-ui
@@ -22,6 +23,7 @@ const MainLayout = () => {
const matchDownXL = useMediaQuery(theme.breakpoints.down('xl'));
const downLG = useMediaQuery(theme.breakpoints.down('lg'));
const { drawerOpen } = useSelector((state) => state.menu);
const { container, miniDrawer, menuOrientation } = useConfig();
const isHorizontal = menuOrientation === MenuOrientation.HORIZONTAL && !downLG;
@@ -39,7 +41,14 @@ const MainLayout = () => {
<Box sx={{ display: 'flex', width: '100%' }}>
<Header />
{!isHorizontal ? <Drawer /> : <HorizontalBar />}
<Box component="main" sx={{ width: 'calc(100% - 260px)', flexGrow: 1, p: { xs: 2, sm: 3 } }}>
<Box
component="main"
sx={{
width: isHorizontal ? '100%' : drawerOpen ? 'calc(100% - 260px)' : { xs: '100%', lg: 'calc(100% - 60px)' },
flexGrow: 1,
p: { xs: 2, sm: 3 }
}}
>
<Toolbar sx={{ mt: isHorizontal ? 8 : 'inherit' }} />
<Container
// maxWidth={container ? 'xl' : false}

View File

@@ -1,3 +1,4 @@
import logger from '../../../utils/logger';
import { enqueueSnackbar } from 'notistack';
import { DeleteFilled, EditOutlined } from '@ant-design/icons';
import { useState, useEffect, Fragment, useRef } from 'react';
@@ -258,18 +259,19 @@ const Deliveries = () => {
// =========================================== || changerider || ===========================================
const changeRiderMutation = useMutation({
mutationFn: ({ selectedRider, selectedRow }) => changeRiderAPI(selectedRider, selectedRow),
onSuccess: (res) => {
onSuccess: (res, { selectedRider, selectedRow }) => {
setLoading1(false);
setChangeDialogOpen(false);
if (res.data.message === 'Success') {
logger.info(`Rider changed successfully for order ID ${selectedRow?.orderid}. New Rider: ${selectedRider?.firstname} ${selectedRider?.lastname}`);
opentoast('Rider Changed Successfully', 'success');
}
fetchCountRefetch(); // Refresh count data
fetchDeliveriesRefetch(); // Refresh deliveries
notifyRiderMutation.mutate(selectedRider.userfcmtoken);
},
onError: (err) => {
console.log(err);
onError: (err, { selectedRider, selectedRow }) => {
logger.error(`Failed to change rider for order ID ${selectedRow?.orderid}:`, err);
opentoast(err.message, 'error');
setLoading1(false);
}
@@ -1236,7 +1238,7 @@ const Deliveries = () => {
Notify Rider
</MenuItem>
)}
{['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && (
{/* {['pending', 'accepted', 'arrived'].includes(selectedRow?.orderstatus) && (
<MenuItem
onClick={() => {
if (!appId) {
@@ -1250,7 +1252,7 @@ const Deliveries = () => {
>
Change Rider
</MenuItem>
)}
)} */}
{(roleid == 1 || roleid == 2) && (
<MenuItem
onClick={() => {
@@ -1487,7 +1489,7 @@ const Deliveries = () => {
renderInput={(params) => <TextField {...params} label="Choose Rider" />}
onChange={(e, value) => {
setSelectedRider(value);
console.log('selected rider', value);
logger.debug('Rider selected in dropdown:', value ? `${value.firstname} ${value.lastname}` : 'None');
}}
/>
</Grid>
@@ -1509,6 +1511,7 @@ const Deliveries = () => {
color="primary"
onClick={() => {
setLoading1(true);
logger.info(`Initiating rider assignment change for order ID ${selectedRow?.orderid} to rider: ${selectedRider?.firstname} ${selectedRider?.lastname}`);
changeRiderMutation.mutate({ selectedRider, selectedRow });
}}
>

File diff suppressed because it is too large Load Diff

View File

@@ -60,6 +60,7 @@ import {
} from './dispatchShared';
import CompareDataPanel from './CompareDataPanel';
import './Dispatch.css';
import logger from '../../../utils/logger';
// Combined-mode rail colors. The per-step palette (STEP_PALETTE) is great for
// "which step is this" but useless for "is this line planned or actual" when
@@ -709,11 +710,11 @@ const Dispatch = ({
// marker; clicking the marker again unpins it. Stored in a ref because this
// only drives imperative leaflet calls — no re-render needed.
const pinnedPopupsRef = useRef(new Set());
// Short-lived close timer for the Compare drop-pin popup. Gives the
// cursor a ~120ms window to travel from the marker onto the popup
// without firing mouseout → closePopup mid-transit. Any new mouseover
// cancels a pending close, so the popup glides instead of flickering.
const compareHoverTimerRef = useRef(null);
// Short-lived close timer for the general map order/marker popups.
// Gives the cursor a ~200ms window to travel from the marker onto the popup
// or vice versa without immediately triggering a close.
const activePopupMarkerRef = useRef(null);
const popupHoverTimerRef = useRef(null);
const isControlled = selectedRiderId !== undefined;
const [clock, setClock] = useState('');
@@ -898,6 +899,7 @@ const Dispatch = ({
}, [appLocations, selectedAppLocationId]);
const handleLocationPick = (id) => {
logger.info('Switching hub/location ID:', id);
setSelectedAppLocationId(Number(id));
setLocationMenuOpen(false);
if (typeof window !== 'undefined') {
@@ -1383,6 +1385,53 @@ const Dispatch = ({
[isControlled, onRiderSelect]
);
// ─── Logger Effects for Rider & Order Updates ───────────────────────
// Log when the active focused rider changes (uncontrolled click or controlled prop change)
const prevFocusedRiderIdRef = useRef(null);
useEffect(() => {
const activeId = focusedRider ? focusedRider.id : null;
if (activeId !== prevFocusedRiderIdRef.current) {
if (focusedRider) {
logger.info(`Focused rider changed to: ${focusedRider.riderName} (${focusedRider.orders.length} orders)`);
} else {
logger.info('Focused rider reset to: None');
}
prevFocusedRiderIdRef.current = activeId;
}
}, [focusedRider]);
// Log when selected focused stop (individual order) changes
useEffect(() => {
if (focusedStop) {
logger.info(`Focused order updated: ID ${focusedStop.orderid}`);
} else {
logger.debug('Focused order selection cleared');
}
}, [focusedStop]);
// Log when periodic query refetch completes and updates the overall orders list
const prevOrdersCountRef = useRef(0);
useEffect(() => {
if (allOrders) {
if (allOrders.length !== prevOrdersCountRef.current) {
logger.info(`Orders database updated: ${allOrders.length} orders actively tracked`);
prevOrdersCountRef.current = allOrders.length;
}
}
}, [allOrders]);
// Log when periodic query refetch completes and updates the live rider locations
const prevRidersCountRef = useRef(0);
useEffect(() => {
if (liveRiderLocations) {
if (liveRiderLocations.length !== prevRidersCountRef.current) {
logger.info(`Live riders list updated: ${liveRiderLocations.length} active riders mapped`);
prevRidersCountRef.current = liveRiderLocations.length;
}
}
}, [liveRiderLocations]);
const activeStats = useMemo(() => {
if (focusedRider) {
return {
@@ -2127,8 +2176,44 @@ const Dispatch = ({
// chips — operators learn the layout once and trust it everywhere.
const renderOrderPopupContent = (o) => {
const statusStyle = getStatusStyle(o.orderstatus);
const isPinned = () => {
if (pinnedPopupsRef.current.has(String(o.orderid))) return true;
if (compareOpen && focusedRider && o.deliveryid != null) {
const track = riderActualTracks.find((t) => String(t.deliveryid) === String(o.deliveryid));
if (track && focusedCompareStep === track.sequenceStep) return true;
}
return false;
};
const handleMouseEnter = () => {
if (popupHoverTimerRef.current) {
clearTimeout(popupHoverTimerRef.current);
popupHoverTimerRef.current = null;
}
};
const handleMouseLeave = () => {
if (isPinned()) return;
if (popupHoverTimerRef.current) {
clearTimeout(popupHoverTimerRef.current);
}
popupHoverTimerRef.current = setTimeout(() => {
if (activePopupMarkerRef.current) {
activePopupMarkerRef.current.closePopup();
activePopupMarkerRef.current = null;
}
popupHoverTimerRef.current = null;
}, 200);
};
return (
<>
<div
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{ height: '100%', width: '100%' }}
>
<div className="pu-header">
<div className="pu-header-top">
<div className="pu-id">ORDER #{o.orderid}</div>
@@ -2234,7 +2319,7 @@ const Dispatch = ({
)}
</div>
</div>
</>
</div>
);
};
@@ -2318,11 +2403,29 @@ const Dispatch = ({
else delete orderMarkerRefs.current[String(o.orderid)];
}}
eventHandlers={{
mouseover: (e) => e.target.openPopup(),
mouseout: (e) => {
if (!pinnedPopupsRef.current.has(String(o.orderid))) {
e.target.closePopup();
mouseover: (e) => {
const marker = e.target;
if (popupHoverTimerRef.current) {
clearTimeout(popupHoverTimerRef.current);
popupHoverTimerRef.current = null;
}
activePopupMarkerRef.current = marker;
marker.openPopup();
},
mouseout: (e) => {
const marker = e.target;
if (pinnedPopupsRef.current.has(String(o.orderid))) return;
if (popupHoverTimerRef.current) {
clearTimeout(popupHoverTimerRef.current);
}
popupHoverTimerRef.current = setTimeout(() => {
marker.closePopup();
if (activePopupMarkerRef.current === marker) {
activePopupMarkerRef.current = null;
}
popupHoverTimerRef.current = null;
}, 200);
},
click: (e) => {
const id = String(o.orderid);
@@ -2336,7 +2439,7 @@ const Dispatch = ({
}
}}
>
<Popup maxWidth={520} minWidth={460} className="dispatch-popup" autoPan={false}>
<Popup maxWidth={520} minWidth={460} className="dispatch-popup" autoPan={true} autoPanPadding={[40, 40]}>
{renderOrderPopupContent(o)}
</Popup>
</Marker>
@@ -2836,13 +2939,13 @@ const Dispatch = ({
)}
<div id="strat-row">
<button className={`sbt ${viewMode === 'kitchens' ? 'active' : ''}`} onClick={() => { setViewMode('kitchens'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPlace /></span> By Location</button>
<button className={`sbt ${viewMode === 'kitchens' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: By Location'); setViewMode('kitchens'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPlace /></span> By Location</button>
<button
className={`sbt ${viewMode === 'zones' ? 'active' : ''}`}
onClick={() => { setViewMode('zones'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}
onClick={() => { logger.info('View mode changed: By Zone'); setViewMode('zones'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}
><span className="sbt-icon"><MdMap /></span> By Zone</button>
<button className={`sbt ${viewMode === 'riders' ? 'active' : ''}`} onClick={() => { setViewMode('riders'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdDirectionsBike /></span> By Rider</button>
<button className={`sbt ${viewMode === 'all' ? 'active' : ''}`} onClick={() => { setViewMode('all'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPublic /></span> All Routes</button>
<button className={`sbt ${viewMode === 'riders' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: By Rider'); setViewMode('riders'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdDirectionsBike /></span> By Rider</button>
<button className={`sbt ${viewMode === 'all' ? 'active' : ''}`} onClick={() => { logger.info('View mode changed: All Routes'); setViewMode('all'); handleRiderFocus(null); setFocusedKitchen(null); setFocusedZone(null); }}><span className="sbt-icon"><MdPublic /></span> All Routes</button>
<button
type="button"
className={`sbt sbt-rider-info ${viewMode === 'rider-info' ? 'active' : ''}`}
@@ -3886,7 +3989,7 @@ const Dispatch = ({
mouseout: (e) => e.target.closePopup()
}}
>
<Popup className="kitchen-popup" maxWidth={220} minWidth={200} autoPan={false}>
<Popup className="kitchen-popup" maxWidth={220} minWidth={200} autoPan={true} autoPanPadding={[20, 20]}>
<div className="kp-header">KITCHEN</div>
<div className="kp-name">{k.kitchenName}</div>
<div className="kp-stat">
@@ -3931,7 +4034,7 @@ const Dispatch = ({
mouseout: (e) => e.target.closePopup()
}}
>
<Popup maxWidth={240} autoPan={false} className="dispatch-popup route-rider-popup">
<Popup maxWidth={240} autoPan={true} autoPanPadding={[20, 20]} className="dispatch-popup route-rider-popup">
<div className="pu-hdr-live">
<div className="pu-hdr-left">
<span className="pu-hdr-title">RIDER ROUTE</span>
@@ -4011,7 +4114,7 @@ const Dispatch = ({
mouseout: (e) => e.target.closePopup()
}}
>
<Popup maxWidth={260} autoPan={false} className="dispatch-popup live-rider-popup">
<Popup maxWidth={260} autoPan={true} autoPanPadding={[20, 20]} className="dispatch-popup live-rider-popup">
<div className="pu-hdr-live">
<div className="pu-hdr-left">
<span className="pu-live-indicator" style={{ '--pulse-color': pinColor }}>
@@ -4272,25 +4375,30 @@ const Dispatch = ({
// the rich order popup, leaving it pinned while
// a step is focused (so click-to-focus keeps the
// modal visible after the cursor moves away). The
// ~120ms grace timer on mouseout lets the cursor
// ~200ms grace timer on mouseout lets the cursor
// travel onto the popup itself without flicker.
mouseover: (e) => {
if (compareHoverTimerRef.current) {
clearTimeout(compareHoverTimerRef.current);
compareHoverTimerRef.current = null;
const marker = e.target;
if (popupHoverTimerRef.current) {
clearTimeout(popupHoverTimerRef.current);
popupHoverTimerRef.current = null;
}
e.target.openPopup();
activePopupMarkerRef.current = marker;
marker.openPopup();
},
mouseout: (e) => {
if (focusedCompareStep === t.sequenceStep) return;
const marker = e.target;
if (compareHoverTimerRef.current) {
clearTimeout(compareHoverTimerRef.current);
if (popupHoverTimerRef.current) {
clearTimeout(popupHoverTimerRef.current);
}
compareHoverTimerRef.current = setTimeout(() => {
popupHoverTimerRef.current = setTimeout(() => {
marker.closePopup();
compareHoverTimerRef.current = null;
}, 120);
if (activePopupMarkerRef.current === marker) {
activePopupMarkerRef.current = null;
}
popupHoverTimerRef.current = null;
}, 200);
},
click: handleEndMarkerClick
}
@@ -4349,7 +4457,8 @@ const Dispatch = ({
maxWidth={520}
minWidth={460}
className="dispatch-popup"
autoPan={false}
autoPan={true}
autoPanPadding={[40, 40]}
>
{renderOrderPopupContent(orderForTrack)}
</Popup>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,226 @@ import { TimePicker } from '@mui/x-date-pickers';
import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
import { MapContainer, TileLayer, Marker, Polyline, useMap } from 'react-leaflet';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
const pickupIcon = typeof window !== 'undefined' ? new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
}) : null;
const dropoffIcon = typeof window !== 'undefined' ? new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
shadowSize: [41, 41]
}) : null;
const MapBoundsController = ({ startPoint, endPoint }) => {
const map = useMap();
useEffect(() => {
const points = [];
const pLat = parseFloat(startPoint?.latitude);
const pLng = parseFloat(startPoint?.longitude);
const dLat = parseFloat(endPoint?.latitude);
const dLng = parseFloat(endPoint?.longitude);
if (!isNaN(pLat) && !isNaN(pLng) && pLat !== 0 && pLng !== 0) {
points.push([pLat, pLng]);
}
if (!isNaN(dLat) && !isNaN(dLng) && dLat !== 0 && dLng !== 0) {
points.push([dLat, dLng]);
}
if (points.length === 1) {
map.setView(points[0], 14);
} else if (points.length === 2) {
map.fitBounds(points, { padding: [50, 50] });
}
}, [startPoint?.latitude, startPoint?.longitude, endPoint?.latitude, endPoint?.longitude, map]);
return null;
};
const OrderMap = ({ startPoint, endPoint, appLocaLat, appLocaLng }) => {
const defaultCenter = [
parseFloat(appLocaLat) || 11.0168,
parseFloat(appLocaLng) || 76.9558
];
const hasPick = startPoint?.latitude && startPoint?.longitude && parseFloat(startPoint.latitude) !== 0;
const hasDrop = endPoint?.latitude && endPoint?.longitude && parseFloat(endPoint.latitude) !== 0;
const pickCoords = hasPick ? [parseFloat(startPoint.latitude), parseFloat(startPoint.longitude)] : null;
const dropCoords = hasDrop ? [parseFloat(endPoint.latitude), parseFloat(endPoint.longitude)] : null;
// ---- Animation state (mirrors Dispatch.js pattern) ----
const [osrmRoute, setOsrmRoute] = useState(null); // full road path [[lat,lng],...]
const [animatedSegments, setAnimatedSegments] = useState([]); // drawn pair-by-pair
const [isAnimating, setIsAnimating] = useState(false);
const isAnimatingRef = useRef(false);
const timeoutsRef = useRef([]);
// Fetch OSRM route whenever both points are available
useEffect(() => {
if (!hasPick || !hasDrop) {
setOsrmRoute(null);
setAnimatedSegments([]);
setIsAnimating(false);
return;
}
const url = `https://router.project-osrm.org/route/v1/driving/${pickCoords[1]},${pickCoords[0]};${dropCoords[1]},${dropCoords[0]}?overview=full&geometries=geojson`;
fetch(url)
.then((r) => r.json())
.then((data) => {
const coords = data?.routes?.[0]?.geometry?.coordinates;
if (coords && coords.length >= 2) {
// OSRM returns [lng, lat], Leaflet needs [lat, lng]
setOsrmRoute(coords.map(([lng, lat]) => [lat, lng]));
} else {
// Fallback to straight line
setOsrmRoute([pickCoords, dropCoords]);
}
})
.catch(() => {
setOsrmRoute([pickCoords, dropCoords]);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [startPoint?.latitude, startPoint?.longitude, endPoint?.latitude, endPoint?.longitude]);
// Reset animation when route changes
useEffect(() => {
setAnimatedSegments([]);
setIsAnimating(false);
isAnimatingRef.current = false;
timeoutsRef.current.forEach(clearTimeout);
timeoutsRef.current = [];
}, [osrmRoute]);
const startAnimation = () => {
// Stop if already animating
if (isAnimating) {
setIsAnimating(false);
isAnimatingRef.current = false;
setAnimatedSegments([]);
timeoutsRef.current.forEach(clearTimeout);
timeoutsRef.current = [];
return;
}
const path = osrmRoute || (hasPick && hasDrop ? [pickCoords, dropCoords] : null);
if (!path || path.length < 2) return;
setIsAnimating(true);
isAnimatingRef.current = true;
setAnimatedSegments([]);
timeoutsRef.current.forEach(clearTimeout);
timeoutsRef.current = [];
// Build segments same way as Dispatch.js: path[i] → path[i+1], each with a delay
const allSegs = [];
for (let i = 0; i < path.length - 1; i++) {
allSegs.push({ from: path[i], to: path[i + 1], delay: i * 18 });
}
allSegs.forEach((s, idx) => {
const t = setTimeout(() => {
if (!isAnimatingRef.current) return;
setAnimatedSegments((prev) => [...prev, s]);
if (idx === allSegs.length - 1) {
const finalT = setTimeout(() => {
setIsAnimating(false);
isAnimatingRef.current = false;
}, 800);
timeoutsRef.current.push(finalT);
}
}, s.delay);
timeoutsRef.current.push(t);
});
};
// Cleanup on unmount
useEffect(() => () => {
timeoutsRef.current.forEach(clearTimeout);
isAnimatingRef.current = false;
}, []);
const staticRoute = osrmRoute || (hasPick && hasDrop ? [pickCoords, dropCoords] : null);
return (
<div style={{ position: 'relative', width: '100%', height: '100%', minHeight: '350px' }}>
<MapContainer
center={defaultCenter}
zoom={12}
style={{ width: '100%', height: '100%', minHeight: '350px' }}
zoomControl={true}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; OpenStreetMap contributors'
/>
{hasPick && <Marker position={pickCoords} icon={pickupIcon} />}
{hasDrop && <Marker position={dropCoords} icon={dropoffIcon} />}
{/* Static solid polyline (shown when not animating) */}
{!isAnimating && staticRoute && staticRoute.length >= 2 && (
<Polyline
positions={staticRoute}
pathOptions={{ color: '#1890ff', weight: 4, opacity: 0.85, lineJoin: 'round', lineCap: 'round' }}
/>
)}
{/* Animated segments — drawn pair-by-pair, matching Dispatch.js pattern */}
{isAnimating && animatedSegments.map((s, i) => (
<Polyline
key={i}
positions={[s.from, s.to]}
pathOptions={{ color: '#1890ff', weight: 5, opacity: 0.95, lineJoin: 'round', lineCap: 'round' }}
/>
))}
<MapBoundsController startPoint={startPoint} endPoint={endPoint} />
</MapContainer>
{/* Floating Animate Routes button — same style as Dispatch's sbt button */}
{hasPick && hasDrop && (
<button
onClick={startAnimation}
style={{
position: 'absolute',
bottom: '16px',
right: '16px',
zIndex: 1000,
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '8px 14px',
borderRadius: '8px',
border: 'none',
cursor: 'pointer',
fontWeight: 600,
fontSize: '13px',
boxShadow: '0 4px 14px rgba(0,0,0,0.18)',
background: isAnimating ? '#1890ff' : '#ffffff',
color: isAnimating ? '#ffffff' : '#1a202c',
transition: 'all 0.25s ease',
letterSpacing: '0.01em'
}}
>
<span style={{ fontSize: '14px' }}>{isAnimating ? '⏹' : '▶'}</span>
{isAnimating ? 'Stop' : 'Animate Route'}
</button>
)}
</div>
);
};
function loadScript(src, position, id) {
if (!position) {
@@ -145,6 +365,8 @@ const Createorder1 = () => {
const [pickDialog, setPickDialog] = useState(false);
const [deliverydate, setDeliverydate] = useState(dayjs().add(30, 'minute'));
const [deliveryDialog, setDeliveryDialog] = useState(false);
const [pickupSearchOpen, setPickupSearchOpen] = useState(false);
const [dropSearchOpen, setDropSearchOpen] = useState(false);
const pickerRef = useRef(null);
const deliveryRef = useRef(null);
@@ -319,7 +541,7 @@ const Createorder1 = () => {
if (tenantid) {
clientdetails();
}
}, [searchCustList?.length > 3, searchCustList == '', tenantid]);
}, [searchCustList?.length > 3, searchCustList == '', tenantid, pickupSearchOpen, dropSearchOpen]);
useEffect(() => {
if (timeslotarr[0]) {
@@ -1037,6 +1259,10 @@ const Createorder1 = () => {
</Stack>
<Grid container spacing={1}>
<Grid item xs={12}>
<Grid container spacing={2} sx={{ height: '100%' }}>
<Grid item xs={12}>
<Grid container spacing={2}>
<Grid item xs={12} md={8}>
<Grid container spacing={2} sx={{ height: '100%' }}>
{/* ================================================= || Pickup || ================================================= */}
@@ -1044,13 +1270,18 @@ const Createorder1 = () => {
<MainCard sx={{ height: '100%' }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Stack direction={'row'} justifyContent={'space-between'}>
<Typography variant="h5" sx={{ mb: 2 }}>
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'}>
<Typography variant="h5" sx={{ mb: 0 }}>
Pickup Details
</Typography>
<Stack display={'flex'} direction={'row'}>
<Stack display={'flex'} direction={'row'} gap={1} alignItems={'center'}>
{pickCust?.firstname && (
<Typography variant="caption" sx={{ color: 'success.main', fontWeight: 600 }}>
Selected
</Typography>
)}
<Button
variant="contained"
variant={pickupSearchOpen ? 'outlined' : 'contained'}
size="small"
disabled={!locationid}
onClick={() => {
@@ -1061,19 +1292,115 @@ const Createorder1 = () => {
} else if (!appId) {
OpenToast('Please select Location!', 'warning', 3000);
} else {
setIsCustomerOpen(true);
setpickordrop(1);
setPickCust(null);
setInputValue1('');
setPickupSearchOpen((prev) => !prev);
setDropSearchOpen(false);
setSearchCustList('');
setpickordrop(1);
}
}}
sx={{ minWidth: 110 }}
>
Select Pickup
{pickupSearchOpen ? 'Close' : 'Select Pickup'}
</Button>
</Stack>
</Stack>
{/* new1 */}
{/* ============ Inline Pickup Search Panel ============ */}
<div
style={{
overflow: 'hidden',
maxHeight: pickupSearchOpen ? '380px' : '0px',
transition: 'max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1)',
marginTop: pickupSearchOpen ? '12px' : '0px'
}}
>
<div
style={{
border: '1.5px solid #e2e8f0',
borderRadius: '12px',
background: '#f8fafc',
padding: '12px',
display: 'flex',
flexDirection: 'column',
gap: '8px'
}}
>
{/* Search Input */}
<OutlinedInput
autoFocus={pickupSearchOpen}
fullWidth
size="small"
placeholder="Search customer..."
value={pickupSearchOpen ? searchCustList : ''}
onChange={(e) => setSearchCustList(e.target.value)}
sx={{ bgcolor: 'white', borderRadius: '8px' }}
startAdornment={
<InputAdornment position="start">
<SearchOutlined style={{ fontSize: 'small' }} />
</InputAdornment>
}
endAdornment={
searchCustList ? (
<IconButton size="small" onClick={() => setSearchCustList('')}>
<ClearIcon fontSize="small" />
</IconButton>
) : null
}
/>
{/* Customer List */}
<div
style={{
maxHeight: '280px',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: '6px'
}}
>
{customerlist?.length === 0 ? (
<Stack alignItems="center" justifyContent="center" sx={{ py: 2 }}>
<Empty />
</Stack>
) : (
customerlist?.map((address, index) => (
<Button
key={index}
disabled={pickCust?.customerid === address.customerid}
onClick={() => {
setPickupSearchOpen(false);
setAddId1(1);
setStartPoint({ latitude: address.latitude, longitude: address.longitude });
setPickCust(address);
setPickNum(address.contactno);
setSearchCustList('');
}}
sx={{
width: '100%',
border: '1.5px solid',
borderColor: pickCust?.customerid === address.customerid ? 'primary.main' : 'grey.200',
borderRadius: '8px',
p: 1.25,
textAlign: 'left',
justifyContent: 'flex-start',
bgcolor: pickCust?.customerid === address.customerid ? 'primary.lighter' : 'white',
'&:hover': { borderColor: 'primary.main', bgcolor: 'rgba(24,144,255,0.04)' }
}}
>
<div style={{ width: '100%' }}>
<Typography variant="subtitle2" sx={{ textAlign: 'left', fontWeight: 600 }}>
{`${address.firstname} (${address.contactno})`}
</Typography>
<Typography variant="caption" color="secondary" sx={{ textAlign: 'left', display: 'block' }}>
{address.address}
</Typography>
</div>
</Button>
))
)}
</div>
</div>
</div>
</Grid>
<Grid item xs={12} sx={{}}>
@@ -1322,12 +1649,18 @@ const Createorder1 = () => {
<MainCard sx={{ height: '100%' }}>
<Grid container spacing={2} sx={{ height: '100%' }}>
<Grid item xs={12} sx={{ height: '100%' }}>
<Stack direction={'row'} justifyContent={'space-between'}>
<Typography variant="h5" sx={{ mb: 2 }}>
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'}>
<Typography variant="h5" sx={{ mb: 0 }}>
Drop Details
</Typography>{' '}
</Typography>
<Stack direction={'row'} gap={1} alignItems={'center'}>
{dropCust?.firstname && (
<Typography variant="caption" sx={{ color: 'success.main', fontWeight: 600 }}>
Selected
</Typography>
)}
<Button
variant="contained"
variant={dropSearchOpen ? 'outlined' : 'contained'}
disabled={!locationid}
size="small"
onClick={() => {
@@ -1338,15 +1671,115 @@ const Createorder1 = () => {
} else if (!appId) {
OpenToast('Please select Location!', 'warning', 3000);
} else {
setIsCustomerOpen(true);
setDropSearchOpen((prev) => !prev);
setPickupSearchOpen(false);
setSearchCustList('');
setpickordrop(2);
setInputValue2('');
}
}}
sx={{ minWidth: 100 }}
>
Select Drop
{dropSearchOpen ? 'Close' : 'Select Drop'}
</Button>
</Stack>
</Stack>
{/* ============ Inline Drop Search Panel ============ */}
<div
style={{
overflow: 'hidden',
maxHeight: dropSearchOpen ? '380px' : '0px',
transition: 'max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1)',
marginTop: dropSearchOpen ? '12px' : '0px'
}}
>
<div
style={{
border: '1.5px solid #e2e8f0',
borderRadius: '12px',
background: '#f8fafc',
padding: '12px',
display: 'flex',
flexDirection: 'column',
gap: '8px'
}}
>
{/* Search Input */}
<OutlinedInput
autoFocus={dropSearchOpen}
fullWidth
size="small"
placeholder="Search customer..."
value={dropSearchOpen ? searchCustList : ''}
onChange={(e) => setSearchCustList(e.target.value)}
sx={{ bgcolor: 'white', borderRadius: '8px' }}
startAdornment={
<InputAdornment position="start">
<SearchOutlined style={{ fontSize: 'small' }} />
</InputAdornment>
}
endAdornment={
searchCustList ? (
<IconButton size="small" onClick={() => setSearchCustList('')}>
<ClearIcon fontSize="small" />
</IconButton>
) : null
}
/>
{/* Customer List */}
<div
style={{
maxHeight: '280px',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: '6px'
}}
>
{customerlist?.length === 0 ? (
<Stack alignItems="center" justifyContent="center" sx={{ py: 2 }}>
<Empty />
</Stack>
) : (
customerlist?.map((address, index) => (
<Button
key={index}
disabled={pickCust?.customerid === address.customerid}
onClick={() => {
setDropSearchOpen(false);
setAddId2(1);
setEndPoint({ latitude: address.latitude, longitude: address.longitude });
setDropCust(address);
setdropNum(address.contactno);
setSearchCustList('');
}}
sx={{
width: '100%',
border: '1.5px solid',
borderColor: dropCust?.customerid === address.customerid ? 'primary.main' : 'grey.200',
borderRadius: '8px',
p: 1.25,
textAlign: 'left',
justifyContent: 'flex-start',
bgcolor: dropCust?.customerid === address.customerid ? 'primary.lighter' : 'white',
'&:hover': { borderColor: 'primary.main', bgcolor: 'rgba(24,144,255,0.04)' }
}}
>
<div style={{ width: '100%' }}>
<Typography variant="subtitle2" sx={{ textAlign: 'left', fontWeight: 600 }}>
{`${address.firstname} (${address.contactno})`}
</Typography>
<Typography variant="caption" color="secondary" sx={{ textAlign: 'left', display: 'block' }}>
{address.address}
</Typography>
</div>
</Button>
))
)}
</div>
</div>
</div>
</Grid>
<Grid item xs={12}>
@@ -1589,6 +2022,22 @@ const Createorder1 = () => {
</Grid>
</MainCard>
</Grid>
</Grid>
</Grid>
{/* ================================================= || Live Map Preview || ================================================= */}
<Grid item xs={12} md={4}>
<MainCard sx={{ height: '100%', minHeight: '400px', display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" sx={{ mb: 2 }}>
Live Route Preview
</Typography>
<div style={{ flex: 1, minHeight: '380px', position: 'relative', borderRadius: '8px' }}>
<OrderMap startPoint={startPoint} endPoint={endPoint} appLocaLat={appLocaLat} appLocaLng={appLocaLng} />
</div>
</MainCard>
</Grid>
</Grid>
</Grid>
{/* ================================================= || Category || ================================================= */}
<Grid item xs={12} sx={{}}>
@@ -2020,133 +2469,7 @@ const Createorder1 = () => {
{/* </Box> */}
</Grid>
{/* ============================================= || saved address Dialog (Customer List)|| ============================================= */}
<Dialog
open={isCustomerOpen}
onClose={() => {
setIsCustomerOpen(false);
setSearchCustList('');
}}
fullWidth
sx={{ minWidth: 'lg' }}
>
<DialogTitle sx={{ bgcolor: theme.palette.primary.main, color: 'white' }}>
<Stack>
<Typography variant="h4"> {`Select Address (${pickordrop === 1 ? 'Pickup' : 'Drop'})`}</Typography>
<FormControl
sx={{
width: '100%',
mt: 1
}}
>
<Stack spacing={2} sx={{ py: 0.2 }}>
<OutlinedInput
autoFocus
fullWidth
id="input-search-header"
placeholder="Search"
value={searchCustList}
onChange={(e) => setSearchCustList(e.target.value)}
sx={{
'& .MuiOutlinedInput-input': {
p: '10.5px 0px 12px'
},
bgcolor: 'white'
}}
startAdornment={
<InputAdornment position="start">
<SearchOutlined style={{ fontSize: 'small' }} />
</InputAdornment>
}
endAdornment={
<IconButton
sx={{ visibility: searchCustList ? 'visible' : 'hidden' }}
onClick={() => {
setSearchCustList('');
}}
>
<ClearIcon />
</IconButton>
}
autoComplete="off"
/>
</Stack>
</FormControl>
</Stack>
</DialogTitle>
<Divider />
<DialogContent sx={{ p: 2.5 }}>
{customerlist?.length == 0 ? (
<Stack spacing={2} direction={'row'} alignItems={'center'} justifyContent={'center'} sx={{}}>
<Empty />
</Stack>
) : (
<Stack spacing={2} sx={{}}>
{customerlist &&
customerlist?.map((address, index) => (
<Button
onClick={() => {
setIsCustomerOpen(false);
if (pickordrop === 1) {
console.log('PickupClient', address);
setAddId1(1);
setStartPoint({ latitude: address.latitude, longitude: address.longitude });
setPickCust(address);
setPickNum(address.contactno);
} else {
console.log('DropClient', address);
setAddId2(1);
setEndPoint({ latitude: address.latitude, longitude: address.longitude });
setDropCust(address);
setdropNum(address.contactno);
}
setSearchCustList('');
}}
disabled={pickCust?.customerid === address.customerid}
key={index}
sx={{
width: '100%',
border: '1px solid',
borderColor: 'grey.200',
borderRadius: 1,
p: 1.25
}}
>
<div style={{ width: '100%' }}>
<Typography variant="subtitle1" sx={{ textAlign: 'left' }}>
{`${address.firstname} (${address.contactno})`}
</Typography>
<Typography variant="body2" color="secondary" sx={{ textAlign: 'left' }}>
{address.address}
</Typography>
</div>
</Button>
))}
</Stack>
)}
</DialogContent>
<Divider />
<DialogActions sx={{ p: 2.5 }}>
<Button
color="error"
variant="outlined"
sx={{
'&:hover': {
bgcolor: 'red',
color: 'white'
}
}}
onClick={() => {
setIsCustomerOpen(false);
setSearchCustList('');
}}
>
Cancel
</Button>
</DialogActions>
</Dialog>
{/* Customer Dialog removed — replaced with inline search panels above */}
{/* ============================================= || location error Dialog || ============================================= */}
<Dialog
open={open}

66
src/utils/logger.js Normal file
View File

@@ -0,0 +1,66 @@
const LOG_LEVELS = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
};
// Default log level based on environment
const currentEnv = process.env.NODE_ENV || 'development';
const isDev = currentEnv === 'development';
const GLOBAL_LOG_LEVEL = isDev ? LOG_LEVELS.DEBUG : LOG_LEVELS.WARN;
const style = (bg, color) => `background: ${bg}; color: ${color}; padding: 2px 5px; border-radius: 4px; font-weight: bold;`;
const PREFIX = '%c[NearlExpress]';
const PREFIX_STYLE = style('#2563eb', '#ffffff');
// Capture original console methods before any global overrides occur
const originalLog = console.log;
const originalWarn = console.warn || console.log;
const originalError = console.error || console.log;
const print = (levelName, args, labelStyle) => {
const levelValue = LOG_LEVELS[levelName];
if (levelValue < GLOBAL_LOG_LEVEL) return;
const [message, ...extra] = args;
const isMessageString = typeof message === 'string';
const formatPrefix = `${PREFIX}%c ${levelName}`;
const styles = [PREFIX_STYLE, labelStyle];
const consoleMethod =
levelName === 'ERROR' ? originalError :
levelName === 'WARN' ? originalWarn :
originalLog;
if (isMessageString) {
consoleMethod(
`${formatPrefix}%c ${message}`,
...styles,
'color: inherit;',
...extra
);
} else {
// If first argument is an object/array, preserve raw interactive log
consoleMethod(
`${formatPrefix}`,
...styles,
message,
...extra
);
}
};
const logger = {
debug: (...args) => print('DEBUG', args, style('#64748b', '#ffffff')),
info: (...args) => print('INFO', args, style('#10b981', '#ffffff')),
warn: (...args) => print('WARN', args, style('#f59e0b', '#ffffff')),
error: (...args) => {
print('ERROR', args, style('#ef4444', '#ffffff'));
// Future expansion hook: e.g., Sentry.captureException(args[0]);
},
};
export default logger;