diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css index 29479bc..7e300c8 100644 --- a/src/pages/nearle/dispatch/Dispatch.css +++ b/src/pages/nearle/dispatch/Dispatch.css @@ -1959,6 +1959,16 @@ padding: 4px 10px; border-radius: 8px; background: var(--bg-sub); + font-variant-numeric: tabular-nums; + font-feature-settings: 'tnum'; + white-space: nowrap; +} + +/* All deliveries done — flip to green so it pops vs the per-rider tint + (mirrors the old right-corner .rchip-n.is-done treatment). */ +.dispatch-container .rcard-badge.is-done { + background: rgba(22, 163, 74, 0.12); + color: #16a34a; } .dispatch-container .bar-bg { @@ -5453,7 +5463,7 @@ border-radius: 14px; box-shadow: 0 18px 40px rgba(15, 23, 42, 0.18); overflow: hidden; - min-width: 460px; + min-width: 580px; animation: dispatch-popup-in 0.18s cubic-bezier(0.4, 0, 0.2, 1); } @@ -5476,7 +5486,77 @@ } .dispatch-container .dispatch-popup .leaflet-popup-content { - min-width: 460px; + min-width: 580px; +} + +/* --- Centered order popup overlay --- + Rendered as a child of .dispatch-container (NOT inside leaflet's + transformed panes), so position: fixed centers on the viewport instead + of inheriting the map's pan offset. Keeps the rich order card fully + visible on small laptop displays where the marker-attached popup would + spill above/below the map and get clipped. */ +.dispatch-container .dispatch-popup-center { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1700; + pointer-events: auto; + max-width: calc(100vw - 32px); + max-height: calc(100vh - 32px); + display: flex; + animation: dispatch-popup-in 0.18s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* The card itself — mirrors the chrome the old leaflet-popup-content-wrapper + provided (rounded corners, soft shadow, hidden overflow) so the inner + .pu-header / .pu-body / .pu-distance-row blocks render identically. */ +.dispatch-container .dispatch-popup-center .dispatch-popup-card { + position: relative; + background: #fff; + border-radius: 14px; + box-shadow: 0 24px 60px rgba(15, 23, 42, 0.28); + /* min() clamps the minimum width so it shrinks gracefully on narrow + viewports instead of forcing horizontal overflow. */ + min-width: min(580px, calc(100vw - 32px)); + max-width: 680px; + max-height: calc(100vh - 32px); + overflow-x: hidden; + overflow-y: auto; +} + +/* Close button — sits in the top-right corner over the purple header. */ +.dispatch-container .dispatch-popup-center-close { + position: absolute; + top: 8px; + right: 8px; + width: 26px; + height: 26px; + border: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + color: #fff; + font-size: 20px; + font-weight: 700; + line-height: 1; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + z-index: 2; + transition: background 0.15s ease; +} + +.dispatch-container .dispatch-popup-center-close:hover { + background: rgba(255, 255, 255, 0.35); +} + +/* Reserve room on the right of the header so the close button doesn't + overlap the status chip. Only applied when the popup is rendered in the + centered overlay (the leaflet-attached variant didn't have a close X). */ +.dispatch-container .dispatch-popup-center .dispatch-popup .pu-header { + padding-right: 44px; } /* --- Header: purple gradient with order id + status + rider --- */ @@ -5559,18 +5639,18 @@ we constrain (via leaflet's maxWidth prop) so the body grows downward as needed for the timeline + details to render in full. */ .dispatch-container .dispatch-popup .pu-body { - padding: 4px 18px 16px; + padding: 4px 16px 12px; } .dispatch-container .dispatch-popup .pu-section { - margin-top: 12px; + margin-top: 8px; } .dispatch-container .dispatch-popup .pu-section-label { /* Scoped override: no horizontal margin since pu-body already provides the gutter. Sits flush with section content. */ - margin: 0 0 8px; - padding-bottom: 6px; + margin: 0 0 6px; + padding-bottom: 4px; font-size: 10px; font-weight: 800; letter-spacing: 0.08em; @@ -5579,18 +5659,21 @@ border-bottom: 1px solid rgba(123, 31, 162, 0.18); } -/* --- Timeline (scoped override of the earlier rules so paddings match - the new pu-body gutter) --- */ +/* --- Timeline: lay events out as a 2-column grid so the 6-row vertical + stack collapses to 3 rows. Keeps the popup short enough to fit on + small-laptop map heights. The connecting line (::before) is hidden + in this layout since the rows no longer form a single column. --- */ .dispatch-container .dispatch-popup .pu-timeline { - padding: 4px 0 4px 4px; - display: flex; - flex-direction: column; - gap: 6px; + padding: 2px 0; + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: 14px; + row-gap: 4px; position: relative; } .dispatch-container .dispatch-popup .pu-timeline::before { - left: 7px; + display: none; } /* --- Details grid: 2 columns of icon/label/value tiles --- */ @@ -5657,8 +5740,8 @@ display: flex; flex-wrap: wrap; gap: 6px; - margin-top: 10px; - padding-top: 10px; + margin-top: 8px; + padding-top: 8px; border-top: 1px dashed rgba(123, 31, 162, 0.18); } diff --git a/src/pages/nearle/dispatch/Dispatch.js b/src/pages/nearle/dispatch/Dispatch.js index ca1fbba..7ece717 100644 --- a/src/pages/nearle/dispatch/Dispatch.js +++ b/src/pages/nearle/dispatch/Dispatch.js @@ -111,32 +111,23 @@ const pickupLat = (o) => o.pickuplat || o.pickuplatitude || o.pickup_lat; const pickupLon = (o) => o.pickuplong || o.pickuplongitude || o.picklongitude || o.pickup_lon; const hasValidPickup = (o) => Number.isFinite(toNum(pickupLat(o))) && Number.isFinite(toNum(pickupLon(o))); -// Named delivery slots — operator's mental model of the day's waves. +// Named delivery batches — operator's mental model of the day's waves. // Each entry covers a half-open range [startHour, endHour) measured in -// FRACTIONAL hours (e.g. 12.5 = 12:30). Half-hour boundaries are supported -// so slot 1 can end at 12:30 PM and slot 2 can start there. -// Slot 5 ends at 24 so anything from 8 PM until midnight buckets there. -// Default slot layout. Used as the seed for the editable slot config the -// operator can tweak at runtime — see slotsConfig state + the slot-edit -// popover below. Don't read BATCHES_DEFAULT directly at runtime; read -// component state instead so user edits take effect. -// Five named waves: -// • Slot 1: morning rush (8 AM → 12:30 PM) -// • Slot 2: lunch (12:20 PM → 3 PM) -// • Slot 3: afternoon (3 PM → 7 PM) -// • Slot 4: evening (7 PM → 8 PM) -// • Slot 5: night (8 PM → midnight) +// FRACTIONAL hours (e.g. 12.5 = 12:30). Half-hour boundaries are supported. +// Three named batches, bucketed by assigntime per spec: +// • Morning Batch: before 8 AM (00:00 → 08:00) +// • Afternoon Batch: 9 AM → 12 PM (09:00 → 12:00) +// • Evening Batch: 4 PM → 7 PM (16:00 → 19:00) +// Gaps (8–9 AM, 12 PM–4 PM, 7 PM+) intentionally fall outside every batch. const BATCHES_DEFAULT_RAW = [ - { id: 'slot-1', startHour: 8, endHour: 12.5 }, - { id: 'slot-2', startHour: 12 + 20 / 60, endHour: 15 }, - { id: 'slot-3', startHour: 15, endHour: 19 }, - { id: 'slot-4', startHour: 19, endHour: 20 }, - { id: 'slot-5', startHour: 20, endHour: 24 } + { id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 }, + { id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12 }, + { id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 } ]; -// v6: the five-named-wave layout with validation checks for array lengths. -// Bumping the key drops cached layouts from v5 and earlier in favour of the new defaults. -const SLOTS_STORAGE_KEY = 'dispatch.slots.v6'; +// v7: three-named-batch layout (Morning / Afternoon / Evening). +// Bumping the key drops cached 5-slot layouts from v6 and earlier. +const SLOTS_STORAGE_KEY = 'dispatch.slots.v7'; // Every prior storage key. Wiped once on mount so stale layouts // from earlier code versions can't reappear on the next page load. @@ -145,7 +136,8 @@ const LEGACY_SLOTS_STORAGE_KEYS = [ 'dispatch.slots.v2', 'dispatch.slots.v3', 'dispatch.slots.v4', - 'dispatch.slots.v5' + 'dispatch.slots.v5', + 'dispatch.slots.v6' ]; // Build a label like "Slot 1 · 8 AM" (or "Slot 2 · 12:30 PM") from a @@ -178,9 +170,12 @@ const formatSlotRange = (startHour, endHour) => { // Doing it through the formatters (instead of hardcoding "Slot 2 · 12:30 PM" // etc.) guarantees user-edited slots and default slots render the same way // — no chance of drift between the two paths. +// Prefer the explicit `name` (e.g. "Morning Batch") when provided; fall back +// to the auto-generated "Slot N · 8 AM" label for any user-added slot that +// doesn't carry a name. const BATCHES_DEFAULT = BATCHES_DEFAULT_RAW.map((s, i) => ({ ...s, - label: formatSlotLabel(i, s.startHour), + label: s.name || formatSlotLabel(i, s.startHour), range: formatSlotRange(s.startHour, s.endHour) })); @@ -196,12 +191,14 @@ const getBatchForHour = (h, batches) => { // timestamp `getRowBatch` reads. "Delivery" defaults to actual deliverytime // with a fallback to expecteddeliverytime so undelivered orders still bucket. const TIME_FIELDS = [ - { id: 'delivery', label: 'Delivery', keys: ['deliverytime', 'expecteddeliverytime'] }, + { id: 'delivered', label: 'Delivered', keys: ['deliverytime'] }, + { id: 'pending', label: 'Pending', keys: ['expecteddeliverytime'] }, { id: 'assigned', label: 'Assigned', keys: ['assigntime'] }, { id: 'accepted', label: 'Accepted', keys: ['acceptedtime'] }, { id: 'started', label: 'Started', keys: ['starttime'] }, { id: 'arrived', label: 'Arrived', keys: ['arrivaltime'] }, - { id: 'pickup', label: 'Pickup', keys: ['pickuptime'] } + { id: 'pickup', label: 'Pickup', keys: ['pickuptime'] }, + { id: 'all', label: 'All', keys: ['deliverytime', 'expecteddeliverytime', 'assigntime', 'acceptedtime', 'arrivaltime', 'pickuptime', 'starttime'] } ]; const getTimeFieldValue = (r, fieldId) => { @@ -212,7 +209,7 @@ const getTimeFieldValue = (r, fieldId) => { return null; }; -const getRowBatch = (r, fieldId = 'delivery', batches = BATCHES_DEFAULT) => { +const getRowBatch = (r, fieldId = 'all', batches = BATCHES_DEFAULT) => { const t = getTimeFieldValue(r, fieldId); if (!t) return null; const str = String(t).trim(); @@ -548,6 +545,22 @@ L.Icon.Default.mergeOptions({ const RIDER_COLORS = ['#0055FF', '#00D82C', '#FF6B00', '#9D00FF', '#FF00A8', '#00C2B2', '#FF9900', '#FF0000']; +// Deterministic rider-id → palette slot mapping. The main `riders` array +// assigns colors by iteration order (see useMemo around line 1308), which +// means the same rider can shuffle to a different color across live-data +// refetches. The Rider Info list needs a fixed color per rider so the dot +// next to "Rajan A" doesn't flip from blue to green on the next poll, so +// it uses this hash-based lookup instead of `getRiderColor(id)`. +const getStableRiderColor = (id) => { + const s = String(id ?? ''); + if (!s) return RIDER_COLORS[0]; + let h = 0; + for (let i = 0; i < s.length; i++) { + h = (h * 31 + s.charCodeAt(i)) >>> 0; + } + return RIDER_COLORS[h % RIDER_COLORS.length]; +}; + // STATUS_STYLES, getStatusStyle, FINAL_STATUSES, SKIPPED_STATUSES, // STEP_PALETTE, stepColor — moved to ./dispatchShared.js so the // extracted CompareDataPanel component can import them without forcing @@ -715,6 +728,13 @@ const Dispatch = ({ // or vice versa without immediately triggering a close. const activePopupMarkerRef = useRef(null); const popupHoverTimerRef = useRef(null); + // Order shown in the centered popup overlay. Rendered outside the leaflet + // map (see `dispatch-popup-center` overlay near the bottom of the JSX) so + // `position: fixed` actually centers on the viewport. Drives behavior that + // used to flow through leaflet's marker-attached : hover opens it, + // mouseout closes it after a ~200ms grace window (unless pinned), click + // toggles a pinned state stored in pinnedPopupsRef. + const [centerPopupOrder, setCenterPopupOrder] = useState(null); const isControlled = selectedRiderId !== undefined; const [clock, setClock] = useState(''); @@ -736,11 +756,11 @@ const Dispatch = ({ const [locationMenuOpen, setLocationMenuOpen] = useState(false); const locationMenuRef = useRef(null); - // Which timestamp column drives slot bucketing. Default = delivery time - // (operator's primary mental model — "did this order land in the X-Y wave?"). - // Switching to Assigned/Accepted/Arrived/Pickup/Started rebuckets every row - // through `getRowBatch(_, selectedTimeField)`. - const [selectedTimeField, setSelectedTimeField] = useState('delivery'); + // Which timestamp column drives slot bucketing. Default = assigntime so + // orders bucket into Morning/Afternoon/Evening by when they were assigned, + // per current spec. The status-wise time-field dropdown is hidden for now + // (see commented-out block in JSX), so this stays fixed at 'assigned'. + const [selectedTimeField, setSelectedTimeField] = useState('assigned'); const [timeFieldMenuOpen, setTimeFieldMenuOpen] = useState(false); const timeFieldMenuRef = useRef(null); @@ -755,18 +775,27 @@ const Dispatch = ({ const raw = window.localStorage.getItem(SLOTS_STORAGE_KEY); if (!raw) return BATCHES_DEFAULT; const parsed = JSON.parse(raw); - // If the parsed slots length does not match BATCHES_DEFAULT_RAW, the data is stale - // (e.g. written from a 3-slot layout version during hot reload). Discard it and load default 5 slots. + // If the parsed slots length does not match BATCHES_DEFAULT_RAW, the data + // is stale (e.g. written from the older 5-slot layout). Discard it and + // load the default 3-batch layout. if (!Array.isArray(parsed) || parsed.length !== BATCHES_DEFAULT_RAW.length) return BATCHES_DEFAULT; // Re-derive label + range from the saved hours so any UI tweaks to the - // formatter (e.g. AM/PM style) flow through to old persisted slots. - return parsed.map((s, i) => ({ - id: s.id || `slot-${i + 1}`, - startHour: Number(s.startHour) || 0, - endHour: Number(s.endHour) || 24, - label: formatSlotLabel(i, Number(s.startHour) || 0), - range: formatSlotRange(Number(s.startHour) || 0, Number(s.endHour) || 24) - })); + // formatter (e.g. AM/PM style) flow through to old persisted slots. If + // the saved id matches a default batch, prefer that batch's friendly + // name ("Morning Batch", etc.) over the generated "Slot N · time" label. + return parsed.map((s, i) => { + const id = s.id || `slot-${i + 1}`; + const startHour = Number(s.startHour) || 0; + const endHour = Number(s.endHour) || 24; + const defaultMatch = BATCHES_DEFAULT.find((b) => b.id === id); + return { + id, + startHour, + endHour, + label: defaultMatch?.name || formatSlotLabel(i, startHour), + range: formatSlotRange(startHour, endHour) + }; + }); } catch (e) { return BATCHES_DEFAULT; } @@ -1956,16 +1985,20 @@ const Dispatch = ({ }, [selectedBatch]); // When the user clicks a step in the focused-rider sidebar (sets focusedStop), - // also open that marker's popup so they see the order details without a second click. - // Wait one frame so MapController has a chance to recenter first. + // surface that order in the centered popup overlay so the details show up + // without a second click. focusedStop is a slim { orderid, lat, lon }, so + // hydrate the full order out of allOrders before passing it to the popup + // (renderOrderPopupContent needs the rich timeline + status fields). + // Wait ~350ms so MapController has a chance to recenter first (matches + // the prior delay before openPopup was called). useEffect(() => { if (!focusedStop) return; const t = setTimeout(() => { - const marker = orderMarkerRefs.current[String(focusedStop.orderid)]; - if (marker && typeof marker.openPopup === 'function') marker.openPopup(); + const fullOrder = allOrders?.find?.((o) => String(o.orderid) === String(focusedStop.orderid)); + if (fullOrder) setCenterPopupOrder(fullOrder); }, 350); return () => clearTimeout(t); - }, [focusedStop]); + }, [focusedStop, allOrders]); const startAnimation = () => { if (isAnimating) { @@ -2152,68 +2185,62 @@ const Dispatch = ({ const getRiderColor = (rid) => riders.find(r => r.id === rid)?.color || '#475569'; // Shared rider-card markup, used in the "By Rider" panel and inside the focused-zone detail. - const renderRiderCard = (r, i) => ( -
handleRiderFocus(r)} style={{ animationDelay: `${i * 0.05}s` }}> -
-
-
-
{r.riderName}
-
{r.orders[0]?.zone_name || locationName || 'Local'} · {new Set(r.orders.map(o => o.trip_number || 1)).size} trips
+ const renderRiderCard = (r, i) => { + const total = r.orders.length; + const delivered = r.orders.filter((o) => + FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase()) + ).length; + const isDone = total > 0 && delivered >= total; + return ( +
handleRiderFocus(r)} style={{ animationDelay: `${i * 0.05}s` }}> +
+
+
+
{r.riderName}
+
{r.orders[0]?.zone_name || locationName || 'Local'} · {new Set(r.orders.map(o => o.trip_number || 1)).size} trips
+
+
+ {delivered}/{total} +
+
+
+
{r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0).toFixed(1)} km₹{r.orders.reduce((s, o) => s + parseFloat(o.profit || 0), 0).toFixed(0)}
+
+ {r.orders.slice(0, 15).map(o => S{o.step})}
-
{r.orders.length}
-
-
{r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0).toFixed(1)} km₹{r.orders.reduce((s, o) => s + parseFloat(o.profit || 0), 0).toFixed(0)}
-
- {r.orders.slice(0, 15).map(o => S{o.step})} -
-
- ); + ); + }; + + // Returns true when the order's centered popup should stay open even after + // the cursor leaves the marker: either explicitly pinned via click, or the + // matching compare-step is focused (so clicking a step in the right panel + // keeps the card visible while the maps recenter). + const isOrderPopupPinned = (o) => { + if (!o) return false; + 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; + }; // Shared order-popup body used by both the planned-route number markers // and the Compare mode actual-track drop pins. Extracted so clicking a // pin on either layer surfaces the exact same Timeline + Details + KM // chips — operators learn the layout once and trust it everywhere. + // Hover behavior (keep-open while cursor is over the card) is owned by + // the centered overlay wrapper, not this function. 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 ( -
+
ORDER #{o.orderid}
@@ -2254,21 +2281,31 @@ const Dispatch = ({
Details
- {o.pickupcustomer && ( + {(o.pickupcustomer || o.locationname || o.pickuplocation) && (
-
Kitchen
-
{o.pickupcustomer}
+
Pickup
+
+ {o.pickupcustomer || o.locationname || o.pickuplocation} +
)} - {(o.locationname || o.pickuplocation) && ( + {(o.deliverysuburb || o.deliveryaddress) && (
-
Pickup
-
{o.locationname || o.pickuplocation}
+
Drop
+
+ {o.deliverysuburb || extractArea(o.deliveryaddress)} +
)} @@ -2403,46 +2440,37 @@ const Dispatch = ({ else delete orderMarkerRefs.current[String(o.orderid)]; }} eventHandlers={{ - mouseover: (e) => { - const marker = e.target; + mouseover: () => { if (popupHoverTimerRef.current) { clearTimeout(popupHoverTimerRef.current); popupHoverTimerRef.current = null; } - activePopupMarkerRef.current = marker; - marker.openPopup(); + setCenterPopupOrder(o); }, - mouseout: (e) => { - const marker = e.target; + mouseout: () => { 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; - } + setCenterPopupOrder((cur) => + cur && String(cur.orderid) === String(o.orderid) ? null : cur + ); popupHoverTimerRef.current = null; }, 200); }, - click: (e) => { + click: () => { const id = String(o.orderid); if (pinnedPopupsRef.current.has(id)) { pinnedPopupsRef.current.delete(id); - e.target.closePopup(); + setCenterPopupOrder(null); } else { pinnedPopupsRef.current.add(id); - e.target.openPopup(); + setCenterPopupOrder(o); } } }} - > - - {renderOrderPopupContent(o)} - - + /> ); }); }; @@ -2963,11 +2991,10 @@ const Dispatch = ({ {shouldFetchLive && viewMode !== 'rider-info' && (
- Slot - {/* Dropdown to pick which timestamp drives slot bucketing. Mirrors - the hub-location dropdown's look so it reads as the same kind of - filter control. The chosen field reruns batchCounts + - filteredLiveRows via selectedTimeField. */} + Batch + {/* Status-wise (time-field) filter is hidden for now per spec — + bucketing is locked to `assigntime`. Restore this block to bring + back the Delivered/Pending/Assigned/... dropdown.
{timeFieldMenuOpen && ( @@ -3006,9 +3033,11 @@ const Dispatch = ({
)}
- {/* Slot editor — lets the operator tweak start/end hours, add a new - slot, remove an existing one, or reset to defaults. Persists via - SLOTS_STORAGE_KEY in localStorage. */} + */} + {/* Slot editor (Edit slots button + panel) is hidden for now per + spec — the three batches (Morning / Afternoon / Evening) are + fixed. Restore this block to bring back the operator-editable + start/end hours, add-slot, and reset-to-defaults controls.
)}
+ */} {/* Inner scroller — keeps the "Slot" label fixed while the chip list scrolls horizontally when it overflows. */}
@@ -3191,7 +3218,7 @@ const Dispatch = ({ className={`ri-rider-item ${isActive ? 'active' : ''}`} onClick={() => setRiderInfoUserid(r.id)} > - + {r.riderName} #{r.id} @@ -4371,32 +4398,29 @@ const Dispatch = ({ eventHandlers={ orderForTrack ? { - // Match the planned-route marker UX: hover opens - // 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 + // Match the planned-route marker UX: hover surfaces + // the rich order card in the centered overlay. The // ~200ms grace timer on mouseout lets the cursor - // travel onto the popup itself without flicker. - mouseover: (e) => { - const marker = e.target; + // travel onto the overlay without flicker. Pinning + // is implicit while focusedCompareStep === this + // step, so the card stays put while the user is + // inspecting this delivery. + mouseover: () => { if (popupHoverTimerRef.current) { clearTimeout(popupHoverTimerRef.current); popupHoverTimerRef.current = null; } - activePopupMarkerRef.current = marker; - marker.openPopup(); + setCenterPopupOrder(orderForTrack); }, - mouseout: (e) => { + mouseout: () => { if (focusedCompareStep === t.sequenceStep) return; - const marker = e.target; if (popupHoverTimerRef.current) { clearTimeout(popupHoverTimerRef.current); } popupHoverTimerRef.current = setTimeout(() => { - marker.closePopup(); - if (activePopupMarkerRef.current === marker) { - activePopupMarkerRef.current = null; - } + setCenterPopupOrder((cur) => + cur && String(cur.orderid) === String(orderForTrack.orderid) ? null : cur + ); popupHoverTimerRef.current = null; }, 200); }, @@ -4452,17 +4476,6 @@ const Dispatch = ({ })()} )} - {orderForTrack && ( - - {renderOrderPopupContent(orderForTrack)} - - )} ); @@ -4508,6 +4521,10 @@ const Dispatch = ({
*/}
+ {/* Right-corner rider/kitchen legend hidden per spec — the same + delivered/total count now renders inside the left sidebar's + rider card badge (see renderRiderCard). Restore this block to + bring back the floating top-right chip list.
{viewMode === 'kitchens' ? ( kitchens.slice(0, 10).map(k => { @@ -4541,6 +4558,7 @@ const Dispatch = ({ }) )}
+ */}
)} + {/* Centered order popup — sibling of the map (NOT inside leaflet's + transformed panes) so position: fixed actually pins it to the + viewport. Replaces the marker-attached leaflet Popup so the rich + order card stays fully visible at the screen center on small + laptop displays. */} + {centerPopupOrder && ( +
{ + if (popupHoverTimerRef.current) { + clearTimeout(popupHoverTimerRef.current); + popupHoverTimerRef.current = null; + } + }} + onMouseLeave={() => { + if (isOrderPopupPinned(centerPopupOrder)) return; + if (popupHoverTimerRef.current) clearTimeout(popupHoverTimerRef.current); + popupHoverTimerRef.current = setTimeout(() => { + setCenterPopupOrder(null); + popupHoverTimerRef.current = null; + }, 200); + }} + > +
+ + {renderOrderPopupContent(centerPopupOrder)} +
+
+ )} +
); }; diff --git a/src/pages/nearle/orders/OrdersRedesign.css b/src/pages/nearle/orders/OrdersRedesign.css index 08cae27..875087c 100644 --- a/src/pages/nearle/orders/OrdersRedesign.css +++ b/src/pages/nearle/orders/OrdersRedesign.css @@ -5,7 +5,7 @@ /* ============================================== */ .location-panel { position: relative; - padding: 16px 18px 16px 18px; + padding: 12px 14px; border-radius: 12px; border: 1px solid #eef2f6; background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%); @@ -39,10 +39,10 @@ display: flex; justify-content: space-between; align-items: center; - gap: 12px; + gap: 10px; flex-wrap: wrap; - margin-bottom: 22px; - padding-bottom: 18px; + margin-bottom: 12px; + padding-bottom: 10px; border-bottom: 1px dashed #e2e8f0; } @@ -154,16 +154,16 @@ } .lp-badge { - width: 42px; - height: 42px; - border-radius: 12px; + width: 34px; + height: 34px; + border-radius: 10px; display: flex; align-items: center; justify-content: center; - font-size: 18px; + font-size: 15px; color: #fff; flex-shrink: 0; - box-shadow: 0 6px 14px rgba(0, 0, 0, 0.10); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.10); } .pickup-panel .lp-badge { @@ -177,25 +177,26 @@ } .lp-title { - font-size: 18px; + font-size: 15px; font-weight: 700; color: #1e293b; line-height: 1.2; } .lp-subtitle { - font-size: 13.5px; + font-size: 11.5px; color: #94a3b8; - margin-top: 3px; + margin-top: 1px; } .lp-action-btn { text-transform: none !important; font-weight: 600 !important; - font-size: 13.5px !important; - border-radius: 10px !important; - padding: 7px 14px !important; + font-size: 12px !important; + border-radius: 8px !important; + padding: 5px 11px !important; letter-spacing: 0.2px !important; + min-height: 30px !important; } .pickup-panel .lp-action-btn { @@ -264,12 +265,12 @@ display: flex; align-items: center; gap: 8px; - font-size: 12.5px; + font-size: 10.5px; font-weight: 700; - letter-spacing: 0.8px; + letter-spacing: 0.7px; text-transform: uppercase; color: #64748b; - margin-bottom: 14px; + margin-bottom: 8px; } .field-group-caption::after { @@ -723,48 +724,53 @@ /* MUI Field Sizing — Readable on large screens */ /* ============================================== */ -/* Bump TextField input + label sizes inside Pickup/Drop panels and order cards */ +/* Compact TextField input + label sizes inside Pickup/Drop panels and order cards */ .location-panel .MuiOutlinedInput-root, .orders-card .MuiOutlinedInput-root { - font-size: 14.5px !important; + font-size: 13px !important; + border-radius: 10px !important; } .location-panel .MuiOutlinedInput-input, .orders-card .MuiOutlinedInput-input { - font-size: 14.5px !important; - padding-top: 11px !important; - padding-bottom: 11px !important; + font-size: 13px !important; + padding-top: 9px !important; + padding-bottom: 9px !important; } .location-panel .MuiInputLabel-root, .orders-card .MuiInputLabel-root { - font-size: 14.5px !important; + font-size: 13px !important; } /* When label is shrunk (floating up), keep it slightly smaller for the float effect */ .location-panel .MuiInputLabel-root.MuiInputLabel-shrink, .orders-card .MuiInputLabel-root.MuiInputLabel-shrink { - font-size: 13px !important; + font-size: 11.5px !important; } /* MUI helper text (validation / hints under fields) */ .location-panel .MuiFormHelperText-root, .orders-card .MuiFormHelperText-root { - font-size: 12.5px !important; + font-size: 11px !important; + margin-top: 3px !important; } /* Autocomplete options dropdown */ .MuiAutocomplete-popper .MuiAutocomplete-option { - font-size: 14.5px !important; + font-size: 13px !important; + padding-top: 6px !important; + padding-bottom: 6px !important; + min-height: 34px !important; } -/* Card section titles (h5 / h6) inside order cards — bump slightly for hierarchy */ +/* Card section titles (h5 / h6) inside order cards — tighter hierarchy */ .orders-card .MuiTypography-h5 { - font-size: 19px !important; + font-size: 15px !important; } .orders-card .MuiTypography-h6 { - font-size: 16.5px !important; + font-size: 13.5px !important; } .orders-card:hover { @@ -900,99 +906,209 @@ overflow: hidden; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); border: 1px solid #eef2f6; - height: 380px; - min-height: 380px; + height: 260px; + min-height: 260px; } -/* Premium Cost & Metrics Dashboard */ +.map-preview-wrapper .leaflet-container { + height: 100% !important; + min-height: 0 !important; +} + +.map-preview-wrapper > div { + min-height: 0 !important; +} + +/* Premium Cost & Metrics Dashboard — compact professional layout */ .pricing-summary-card { background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%) !important; border: 1px solid #eef2f6 !important; - border-radius: 16px !important; - padding: 20px !important; + border-radius: 14px !important; + padding: 14px 16px !important; +} + +.pricing-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid #f1f5f9; +} + +.pricing-title { + font-size: 14px !important; + font-weight: 700 !important; + color: #1e293b !important; + letter-spacing: -0.01em; +} + +.pricing-subtitle { + font-size: 11px !important; + font-weight: 500 !important; + color: #94a3b8 !important; + text-transform: uppercase; + letter-spacing: 0.6px; } .price-metric-item { display: flex; align-items: center; justify-content: space-between; - padding: 12px 0; - border-bottom: 1px dashed #e2e8f0; + padding: 7px 0; + border-bottom: 1px solid #f5f7fa; } -.price-metric-item:last-child { +.price-metric-item:last-of-type { border-bottom: none; + padding-bottom: 4px; } .price-metric-label { - font-size: 14.5px; + font-size: 12.5px; color: #475569; font-weight: 500; display: flex; align-items: center; - gap: 8px; + gap: 10px; + min-width: 0; +} + +.price-metric-icon { + width: 26px; + height: 26px; + flex-shrink: 0; + border-radius: 8px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; +} + +.price-metric-icon.icon-distance { + background: rgba(24, 144, 255, 0.10); + color: #1890ff; +} + +.price-metric-icon.icon-base { + background: rgba(34, 197, 94, 0.10); + color: #16a34a; +} + +.price-metric-icon.icon-rate { + background: rgba(245, 158, 11, 0.12); + color: #d97706; +} + +.price-metric-sub { + color: #94a3b8; + font-weight: 500; + font-size: 11.5px; } .price-metric-value { - font-size: 16px; + font-size: 13.5px; font-weight: 700; color: #1e293b; + display: inline-flex; + align-items: baseline; + gap: 2px; + white-space: nowrap; } .price-metric-value.highlight { color: #1890ff; } +.price-metric-unit { + font-size: 11px; + font-weight: 500; + color: #94a3b8; + margin-left: 2px; +} + .total-charge-badge { - background: linear-gradient(135deg, rgba(24, 144, 255, 0.08) 0%, rgba(101, 56, 122, 0.08) 100%); - border: 1px solid rgba(24, 144, 255, 0.15); - border-radius: 12px; - padding: 16px; + background: linear-gradient(135deg, rgba(24, 144, 255, 0.08) 0%, rgba(101, 56, 122, 0.10) 100%); + border: 1px solid rgba(101, 56, 122, 0.18); + border-radius: 10px; + padding: 10px 14px; display: flex; - flex-direction: column; align-items: center; - margin-top: 16px; + justify-content: space-between; + gap: 12px; + margin-top: 12px; +} + +.total-charge-left { + display: inline-flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.total-charge-icon { + font-size: 13px; + color: #65387A; + flex-shrink: 0; } .total-charge-label { - font-size: 13px; + font-size: 11.5px; font-weight: 700; text-transform: uppercase; - letter-spacing: 0.8px; + letter-spacing: 0.6px; color: #65387A; - margin-bottom: 6px; } .total-charge-val { - font-size: 32px; + font-size: 20px; font-weight: 800; color: #65387A; line-height: 1.1; + letter-spacing: -0.01em; + white-space: nowrap; } -/* Gradient Action Button */ +/* Gradient Action Button — compact professional */ .gradient-btn-create { background: linear-gradient(135deg, #1890ff 0%, #65387a 100%) !important; color: #ffffff !important; font-weight: 600 !important; - border-radius: 12px !important; - padding: 12px 28px !important; - box-shadow: 0 8px 20px -4px rgba(24, 144, 255, 0.3) !important; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + font-size: 13px !important; + letter-spacing: 0.01em !important; + text-transform: none !important; + border-radius: 10px !important; + padding: 8px 18px !important; + min-height: 38px !important; + box-shadow: 0 4px 12px -3px rgba(24, 144, 255, 0.30), 0 2px 4px rgba(101, 56, 122, 0.10) !important; + transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1) !important; border: none !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + gap: 8px !important; +} + +.gradient-btn-create .MuiButton-startIcon, +.gradient-btn-create .MuiButton-endIcon { + margin: 0 !important; } .gradient-btn-create:hover { - transform: translateY(-2px) !important; - box-shadow: 0 12px 28px -4px rgba(24, 144, 255, 0.45), 0 4px 10px rgba(101, 56, 122, 0.2) !important; + transform: translateY(-1px) !important; + filter: brightness(1.04); + box-shadow: 0 8px 18px -4px rgba(24, 144, 255, 0.40), 0 3px 8px rgba(101, 56, 122, 0.18) !important; } .gradient-btn-create:active { transform: translateY(0) !important; + filter: brightness(0.98); } +.gradient-btn-create.Mui-disabled, .gradient-btn-create:disabled { - background: #cbd5e1 !important; + background: #e2e8f0 !important; color: #94a3b8 !important; box-shadow: none !important; cursor: not-allowed !important; @@ -1261,8 +1377,8 @@ } .map-preview-wrapper { - height: 300px; - min-height: 300px; + height: 220px; + min-height: 220px; } .weight-card-btn { @@ -1299,4 +1415,464 @@ main:has(.orders-workspace-bg) { padding: 16px !important; } +} + +/* ============================================== */ +/* Compact header dropdowns (Location / Client / Business Location) */ +/* ============================================== */ +.header-compact-tf .MuiOutlinedInput-root { + border-radius: 10px !important; + height: 40px !important; + padding-left: 10px !important; + font-size: 12.5px !important; + background: #ffffff; +} + +.header-compact-tf .MuiOutlinedInput-input { + padding-top: 6px !important; + padding-bottom: 6px !important; + font-size: 12.5px !important; +} + +.header-compact-tf .MuiInputLabel-root { + font-size: 11.5px !important; + letter-spacing: 0.02em; + font-weight: 600; + color: #64748b !important; +} + +.header-compact-tf .MuiInputLabel-shrink { + transform: translate(12px, -7px) scale(0.82) !important; + background: #ffffff; + padding: 0 4px; +} + +.header-compact-tf .MuiOutlinedInput-notchedOutline { + border-color: #e2e8f0; +} + +.header-compact-tf:hover .MuiOutlinedInput-notchedOutline { + border-color: #cbd5e1; +} + +.header-compact-tf .Mui-focused .MuiOutlinedInput-notchedOutline { + border-width: 1.5px !important; +} + +/* Autocomplete-specific tweaks: vertically center the clear / popup icons */ +.header-compact-input .MuiAutocomplete-endAdornment { + top: 50%; + transform: translateY(-50%); + right: 8px; + display: inline-flex; + align-items: center; + height: auto; + gap: 2px; +} + +.header-compact-input .MuiAutocomplete-endAdornment .MuiSvgIcon-root { + font-size: 16px; + display: block; +} + +.header-compact-input .MuiAutocomplete-clearIndicator, +.header-compact-input .MuiAutocomplete-popupIndicator { + padding: 3px !important; + width: 22px; + height: 22px; + display: inline-flex; + align-items: center; + justify-content: center; + color: #94a3b8 !important; +} + +.header-compact-input .MuiAutocomplete-clearIndicator:hover, +.header-compact-input .MuiAutocomplete-popupIndicator:hover { + background: rgba(148, 163, 184, 0.12) !important; + color: #475569 !important; +} + +.header-compact-input .MuiAutocomplete-popupIndicator { + margin-right: 0; +} + +.header-compact-input .MuiOutlinedInput-root { + padding-top: 0 !important; + padding-bottom: 0 !important; + padding-right: 60px !important; +} + +.header-compact-input .MuiAutocomplete-input { + padding: 4px 4px 4px 0 !important; + height: auto !important; +} + +/* Title row alignment tweak for tighter header */ +.page-header-row { + min-height: 0 !important; +} + +/* ============================================== */ +/* Delivery Preferences Card */ +/* (Special Dispatch Notes + SMS Updates) */ +/* ============================================== */ +.delivery-prefs-card { + background: linear-gradient(135deg, #ffffff 0%, #fbfcff 100%) !important; + border: 1px solid #eef2f6 !important; + border-radius: 14px !important; +} + +.delivery-prefs-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid #f1f5f9; +} + +.delivery-prefs-title { + font-size: 14px !important; + font-weight: 700 !important; + color: #1e293b !important; + letter-spacing: -0.01em; + line-height: 1.2; +} + +.delivery-prefs-sub { + font-size: 10.5px !important; + font-weight: 500 !important; + color: #94a3b8 !important; + text-transform: uppercase; + letter-spacing: 0.55px; + text-align: right; + line-height: 1.2; +} + +.delivery-prefs-row { + display: flex; + flex-direction: column; + gap: 10px; +} + +.delivery-prefs-field { + display: flex; + flex-direction: column; + gap: 5px; +} + +.delivery-prefs-label { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 10.5px; + font-weight: 700; + letter-spacing: 0.55px; + text-transform: uppercase; + color: #64748b; +} + +/* SMS toggle tile — a card-like clickable strip */ +.sms-toggle-tile { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 8px 12px; + border-radius: 10px; + border: 1px solid #eef2f6; + background: #fafbfc; + cursor: pointer; + user-select: none; + transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease; +} + +.sms-toggle-tile:hover { + border-color: #cbd5e1; + background: #ffffff; +} + +.sms-toggle-tile.is-active { + background: linear-gradient(135deg, rgba(24, 144, 255, 0.06) 0%, rgba(101, 56, 122, 0.05) 100%); + border-color: rgba(24, 144, 255, 0.28); + box-shadow: 0 3px 10px -3px rgba(24, 144, 255, 0.18); +} + +.sms-toggle-left { + display: inline-flex; + align-items: center; + gap: 9px; + min-width: 0; +} + +.sms-toggle-icon { + width: 28px; + height: 28px; + flex-shrink: 0; + border-radius: 8px; + display: inline-flex; + align-items: center; + justify-content: center; + background: #eef2f6; + color: #94a3b8; + transition: all 0.2s ease; +} + +.sms-toggle-tile.is-active .sms-toggle-icon { + background: linear-gradient(135deg, #1890ff, #65387a); + color: #ffffff; + box-shadow: 0 3px 10px rgba(101, 56, 122, 0.22); +} + +.sms-toggle-title { + font-size: 12.5px !important; + font-weight: 700 !important; + color: #1e293b !important; + line-height: 1.2 !important; + letter-spacing: -0.005em; +} + +.sms-toggle-sub { + font-size: 10.5px !important; + color: #94a3b8 !important; + font-weight: 500 !important; + margin-top: 1px !important; + line-height: 1.2 !important; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sms-toggle-tile .MuiSwitch-root { + flex-shrink: 0; +} + +/* ============================================== */ +/* Pickup → Drop Two-Step Stepper */ +/* ============================================== */ +.route-stepper { + display: flex; + align-items: stretch; + gap: 0; + padding: 4px; + margin-bottom: 12px; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + border: 1px solid #e2e8f0; + border-radius: 12px; +} + +.route-step { + flex: 1; + display: flex; + align-items: center; + gap: 10px; + padding: 6px 10px; + border-radius: 9px; + cursor: pointer; + transition: background-color 0.22s ease, box-shadow 0.22s ease, transform 0.22s ease; + user-select: none; + background: transparent; + outline: none; +} + +.route-step:hover { + background: rgba(255, 255, 255, 0.6); +} + +.route-step.is-active { + background: #ffffff; + box-shadow: 0 6px 18px -8px rgba(15, 23, 42, 0.12), 0 2px 6px -2px rgba(15, 23, 42, 0.06); + transform: translateY(-1px); +} + +.route-step.is-locked { + cursor: not-allowed; + opacity: 0.6; +} + +.route-step.is-locked:hover { + background: transparent; +} + +.route-step-index { + width: 26px; + height: 26px; + flex-shrink: 0; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 12px; + color: #94a3b8; + background: #ffffff; + border: 1.5px solid #e2e8f0; + transition: all 0.22s ease; +} + +.step-pickup.is-active .route-step-index { + background: linear-gradient(135deg, #1890ff, #096dd9); + border-color: transparent; + color: #ffffff; + box-shadow: 0 4px 12px rgba(24, 144, 255, 0.32); +} + +.step-drop.is-active .route-step-index { + background: linear-gradient(135deg, #a855f7, #65387a); + border-color: transparent; + color: #ffffff; + box-shadow: 0 4px 12px rgba(101, 56, 122, 0.32); +} + +.step-pickup.is-done:not(.is-active) .route-step-index { + background: rgba(34, 197, 94, 0.12); + border-color: rgba(34, 197, 94, 0.35); + color: #16a34a; +} + +.route-step-text { + display: flex; + flex-direction: column; + line-height: 1.2; +} + +.route-step-title { + font-size: 13px !important; + font-weight: 700 !important; + color: #1e293b !important; + letter-spacing: -0.01em; +} + +.route-step.is-locked .route-step-title { + color: #94a3b8 !important; +} + +.route-step-sub { + font-size: 10.5px !important; + font-weight: 500 !important; + color: #94a3b8 !important; + margin-top: 1px !important; +} + +.route-step-connector { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 0 6px; + position: relative; + min-width: 28px; +} + +.route-step-line { + width: 100%; + height: 2px; + background: #e2e8f0; + border-radius: 2px; + transition: background 0.3s ease; +} + +.route-step-connector.is-done .route-step-line { + background: linear-gradient(90deg, #1890ff, #a855f7); +} + +.route-step-line-arrow { + position: absolute; + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 50%; + background: #ffffff; + border: 1.5px solid #e2e8f0; + color: #cbd5e1; + font-size: 10px; + transition: all 0.3s ease; +} + +.route-step-connector.is-done .route-step-line-arrow { + border-color: rgba(168, 85, 247, 0.4); + color: #a855f7; +} + +/* Step navigation footer inside each panel */ +.step-nav { + margin-top: 12px; + padding-top: 10px; + border-top: 1px dashed #e2e8f0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + flex-wrap: wrap; +} + +.step-nav-hint { + font-size: 11.5px !important; + color: #64748b !important; + font-weight: 500 !important; +} + +.step-nav-btn { + text-transform: none !important; + font-weight: 600 !important; + border-radius: 8px !important; + padding: 6px 14px !important; + font-size: 12px !important; + letter-spacing: 0.01em !important; + transition: all 0.22s ease !important; + min-height: 32px !important; +} + +.step-nav-next { + background: linear-gradient(135deg, #1890ff, #65387a) !important; + color: #ffffff !important; + box-shadow: 0 6px 18px -6px rgba(101, 56, 122, 0.35) !important; +} + +.step-nav-next:hover { + filter: brightness(1.05); + transform: translateY(-1px); + box-shadow: 0 10px 22px -8px rgba(101, 56, 122, 0.45) !important; +} + +.step-nav-next.Mui-disabled { + background: #e2e8f0 !important; + color: #94a3b8 !important; + box-shadow: none !important; +} + +.step-nav-back { + color: #475569 !important; + background: #f1f5f9 !important; + border: 1px solid #e2e8f0 !important; +} + +.step-nav-back:hover { + background: #e2e8f0 !important; + color: #1e293b !important; +} + +@media (max-width: 599px) { + .route-step-sub { + display: none !important; + } + .route-step { + padding: 8px 10px; + gap: 8px; + } + .route-step-index { + width: 28px; + height: 28px; + font-size: 13px; + } + .step-nav { + flex-direction: column-reverse; + align-items: stretch; + } + .step-nav-btn { + width: 100%; + } } \ No newline at end of file diff --git a/src/pages/nearle/orders/createorder1.js b/src/pages/nearle/orders/createorder1.js index 83d91ac..503fd10 100644 --- a/src/pages/nearle/orders/createorder1.js +++ b/src/pages/nearle/orders/createorder1.js @@ -26,7 +26,7 @@ import { } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; import { Empty } from 'antd'; -import { FaPhoneAlt, FaBox, FaBoxes, FaTruck } from 'react-icons/fa'; +import { FaPhoneAlt, FaBox, FaBoxes, FaTruck, FaArrowRight, FaArrowLeft, FaCheck, FaRoute, FaMoneyBillWave, FaChartLine, FaReceipt, FaPaperPlane } from 'react-icons/fa'; import { GiDoorHandle } from 'react-icons/gi'; import { FaLandmarkDome } from 'react-icons/fa6'; import ClearIcon from '@mui/icons-material/Clear'; @@ -143,11 +143,11 @@ const OrderMap = ({ startPoint, endPoint, appLocaLat, appLocaLng }) => { }, [startPoint.latitude, startPoint.longitude, endPoint.latitude, endPoint.longitude, hasPick, hasDrop]); return ( -
+
{ const [locationValue, setLocationValue] = useState(null); const [pickupSlotsList, setPickupSlotsList] = useState(null); const [pickupSlot, setPickupSlot] = useState(null); + const [routeStep, setRouteStep] = useState(1); // 1 = Pickup, 2 = Drop + const pickupStepComplete = !!( + pickCust?.firstname && + pickCust?.contactno && + String(pickCust.contactno).length === 10 && + pickCust?.doorno && + pickCust?.suburb && + pickCust?.postcode + ); useEffect(() => { console.log('pickupSlotsList', pickupSlotsList); @@ -1060,14 +1069,14 @@ const Createorder1 = () => { @@ -1076,19 +1085,16 @@ const Createorder1 = () => { sx={{ display: 'flex', flexDirection: 'column', - gap: 0.5, + gap: 0, minWidth: 0, flex: { lg: '1 1 auto' } }} > - - + + Create New Order - - Configure client coordinates, delivery payloads, and dispatch schedules in real-time. - { flexDirection: { xs: 'column', sm: 'row' }, alignItems: { xs: 'stretch', sm: 'center' }, justifyContent: { xs: 'stretch', lg: 'flex-end' }, - gap: 1.5, - p: { xs: 0.5, sm: 1, lg: 1.5 }, + gap: 1, + p: 0, width: { xs: '100%', lg: 'auto' }, flexShrink: 0 }} > {/* Choose App location */} - + { ref={locationRef} options={locations || []} getOptionLabel={(option) => `${option.locationname}`} + className="header-compact-input" onChange={(event, value, reason) => { if (reason === 'clear') { setAppId(0); @@ -1142,12 +1149,12 @@ const Createorder1 = () => { placeholder="Choose Location" label="Location" InputLabelProps={{ shrink: true }} - sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', paddingLeft: '12px' } }} + className="header-compact-tf" InputProps={{ ...params.InputProps, startAdornment: ( <> - + {params.InputProps.startAdornment} ) @@ -1158,10 +1165,11 @@ const Createorder1 = () => { {/* Choose Client */} - + { @@ -1200,12 +1208,12 @@ const Createorder1 = () => { label="Client" inputRef={tenantRef} InputLabelProps={{ shrink: true }} - sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', paddingLeft: '12px' } }} + className="header-compact-tf" InputProps={{ ...params.InputProps, startAdornment: ( <> - + {params.InputProps.startAdornment} ) @@ -1216,7 +1224,7 @@ const Createorder1 = () => { {/* Business Location */} - + {tenantLocations.length == 1 ? ( { label="Business Location" value={tenantLocations[0].locationname} InputLabelProps={{ shrink: true }} - sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', paddingLeft: '12px' } }} + className="header-compact-tf" InputProps={{ style: { color: theme.palette.primary.main }, startAdornment: ( - + ) }} /> @@ -1237,6 +1245,7 @@ const Createorder1 = () => { `${option.locationname} (${option.suburb})` || ''} @@ -1275,12 +1284,12 @@ const Createorder1 = () => { label="Business Location" color="primary" InputLabelProps={{ shrink: true }} - sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', paddingLeft: '12px' } }} + className="header-compact-tf" InputProps={{ ...params.InputProps, startAdornment: ( <> - + {params.InputProps.startAdornment} ) @@ -1304,17 +1313,64 @@ const Createorder1 = () => { sx={{ display: 'flex', flexDirection: 'column', - gap: { xs: 2.5, sm: 3, lg: 4 } + gap: { xs: 1.5, sm: 1.75, lg: 2 } }} > {/* Card 2: Route Planner (Pickup & Drop) — tighter padding on md+ since the two panels now sit side-by-side and need the horizontal room. */} - + + {/* Two-step stepper: Pickup → Drop */} + + setRouteStep(1)} + role="button" + tabIndex={0} + > + + {pickupStepComplete && routeStep !== 1 ? : '1'} + + + Pickup + Where to collect + + + + + + + + + + + { + if (pickupStepComplete) { + setRouteStep(2); + } else { + opentoast('Please complete Pickup details first', 'warning', 2000); + } + }} + role="button" + tabIndex={0} + > + 2 + + Drop + Where to deliver + + + + {/* Pickup Details Block */} - + @@ -1356,7 +1412,7 @@ const Createorder1 = () => { Contact - + { {/* Address Autocomplete */} - + Address Lookup {addId1 == 0 ? ( @@ -1508,10 +1564,10 @@ const Createorder1 = () => { )} {/* Address details */} - + Address Details - + { {/* Save for later */} {showCheck1 == 1 && ( - + { )} + + {/* Step navigation */} + + + {pickupStepComplete + ? 'Pickup looks good. Proceed to Drop details.' + : 'Fill the required Pickup fields to continue.'} + + + {/* Drop Details Block */} - + @@ -1656,7 +1732,7 @@ const Createorder1 = () => { Contact - + { {/* Address Autocomplete */} - + Address Lookup {addId2 == 0 ? ( @@ -1806,10 +1882,10 @@ const Createorder1 = () => { )} {/* Address details */} - + Address Details - + { {/* Save for later */} {showCheck2 == 1 && ( - + { )} + + {/* Step navigation */} + + + + Review the route below once Drop is filled. + + {/* Card 3: Cargo & Dispatch Logistics */} - - - - Cargo & Dispatch Logistics + + + + Cargo & Dispatch Logistics - + {/* Section Header: Cargo Details */} - - - + + + Cargo Details @@ -1946,7 +2036,7 @@ const Createorder1 = () => { sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', - height: '42px', + height: '38px', paddingTop: '0px !important', paddingBottom: '0px !important' } @@ -1957,7 +2047,7 @@ const Createorder1 = () => { label="Category" size="small" InputLabelProps={{ shrink: true }} - sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '42px' } }} + sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '38px' } }} InputProps={{ ...params.InputProps, startAdornment: ( @@ -1996,7 +2086,7 @@ const Createorder1 = () => { setCollectionamt(e.target.value); }} inputProps={{ min: 0 }} - sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '42px' } }} + sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '38px' } }} InputProps={{ startAdornment: ( @@ -2021,7 +2111,7 @@ const Createorder1 = () => { setQuantity(e.target.value); }} inputProps={{ min: 1 }} - sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '42px' } }} + sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '38px' } }} InputProps={{ startAdornment: ( @@ -2033,73 +2123,20 @@ const Createorder1 = () => { - {/* Row 2: Weight Range Selector */} - - - - Select Cargo Weight Range * - - {weight && ( - - {weight === '1-10kgs' ? 'Light' : weight === '11-20kgs' ? 'Medium' : 'Heavy'} selected - - )} - -
-
{ - handleChipClick('1-10kgs'); - setWeight('1-10kgs'); - }} - > - -
Light Cargo (1-10 kgs)
-
Parcels, retail envelopes
-
-
{ - handleChipClick('11-20kgs'); - setWeight('11-20kgs'); - }} - > - -
Medium Cargo (11-20 kgs)
-
Grocery crates, retail goods
-
-
{ - handleChipClick('21-30kgs'); - setWeight('21-30kgs'); - }} - > - -
Heavy Cargo (21-30 kgs)
-
Industrial parts, heavy shipments
-
-
-
{/* Section Header: Handover & Schedule */} - - - + + + Schedule Details - {/* Nested Grid Container with tighter spacing={2} to eliminate excessive gaps */} + {/* Nested Grid Container with tight spacing to eliminate excessive gaps */} - + {/* Row 3: Pickup Date & Time Slot (Side-by-Side) */} @@ -2130,7 +2167,7 @@ const Createorder1 = () => { width: '100%', '& .MuiOutlinedInput-root': { borderRadius: '12px', - height: '42px' + height: '38px' } }} slotProps={{ @@ -2148,7 +2185,7 @@ const Createorder1 = () => { sx: { '& .MuiOutlinedInput-root': { borderRadius: '12px', - height: '42px', + height: '38px', paddingLeft: '10px' } } @@ -2168,7 +2205,7 @@ const Createorder1 = () => { width: '100%', '& .MuiOutlinedInput-root': { borderRadius: '12px', - height: '42px', + height: '38px', paddingTop: '0px !important', paddingBottom: '0px !important' } @@ -2194,7 +2231,7 @@ const Createorder1 = () => { placeholder="Select Pickup Slot" fullWidth InputLabelProps={{ shrink: true }} - sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '42px' } }} + sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '38px' } }} InputProps={{ ...params.InputProps, startAdornment: ( @@ -2209,95 +2246,6 @@ const Createorder1 = () => { /> - {/* Row 4: Special Dispatch Notes & SMS Updates (Side-by-Side) */} - - - - - ) - }} - placeholder="Provide gate codes, call instructions, or special cargo care instructions..." - value={otherinstructions} - onChange={(e) => setOtherinstructions(e.target.value)} - /> - - - - setIsSms(isSms === 1 ? 0 : 1)} - sx={{ - py: 0, - px: 2, - height: '42px', - border: '1.5px solid #eef2f6', - borderRadius: '12px', - cursor: 'pointer', - userSelect: 'none', - bgcolor: isSms === 1 ? 'rgba(24, 144, 255, 0.04)' : '#fafbfc', - borderColor: isSms === 1 ? 'rgba(24, 144, 255, 0.25)' : '#eef2f6', - boxShadow: isSms === 1 ? '0 4px 12px rgba(24, 144, 255, 0.05)' : 'none', - transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)', - '&:hover': { - transform: 'translateY(-1.2px)', - borderColor: isSms === 1 ? 'rgba(24, 144, 255, 0.4)' : '#cbd5e1', - boxShadow: isSms === 1 ? '0 6px 16px rgba(24, 144, 255, 0.08)' : '0 6px 16px rgba(0, 0, 0, 0.04)' - } - }} - > - - - - - - - SMS Updates - - - Track automatically - - - - { - e.stopPropagation(); - setIsSms(e.target.checked ? 1 : 0); - }} - /> - - @@ -2315,12 +2263,12 @@ const Createorder1 = () => { height: 'fit-content' }} > - + {/* Map Card */} - - - + + + Live Route Preview
@@ -2328,24 +2276,99 @@ const Createorder1 = () => {
+ {/* Delivery Preferences — Dispatch Notes & SMS Updates */} + + + Delivery Preferences + Customer notifications & dispatch instructions + + + + + + setOtherinstructions(e.target.value)} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: '10px', + padding: '0 10px', + alignItems: 'center', + fontSize: '12px', + background: '#ffffff', + height: '32px' + }, + '& .MuiOutlinedInput-input': { + padding: '0 !important', + fontSize: '12px !important', + lineHeight: '32px' + } + }} + /> + + + setIsSms(isSms === 1 ? 0 : 1)} + role="button" + tabIndex={0} + > + + + + + + SMS Updates + Auto-notify customer on dispatch & delivery + + + { + e.stopPropagation(); + setIsSms(e.target.checked ? 1 : 0); + }} + /> + + + + {/* Pricing breakdown card */} - - Pricing & Dispatch Metrics - + + Pricing & Dispatch + Live cost estimate +
- 📍 Delivery Distance + + + + Delivery Distance
- {showDistance ? `${distance} km` : '--'} + {showDistance ? `${distance} km` : '—'}
- 💵 Base Fare ({minKm} km limit) + + + + + Base Fare + · {minKm} km +
{basePrice ? `₹${basePrice.toFixed(2)}` : '₹0.00'} @@ -2354,28 +2377,36 @@ const Createorder1 = () => {
- 📈 Rate per km + + + + Rate per km
- {pricePerKm ? `₹${pricePerKm.toFixed(2)}/km` : '₹0.00/km'} + {pricePerKm ? `₹${pricePerKm.toFixed(2)}` : '₹0.00'} + /km
{/* Total Cost Display */} {showDistance && (
-
Total Delivery Charge
+
+ +
Total Delivery Charge
+
₹{totalCharge.toFixed(2)}
)} {/* Submit button */} - +