From f5307dfb03bd5c6af4b8d3853fb95e5f0259f07f Mon Sep 17 00:00:00 2001 From: dharaneesh-r Date: Mon, 25 May 2026 15:39:18 +0530 Subject: [PATCH] updates on the ui changes and updates on the delivery page updated the status --- src/pages/nearle/deliveries/deliveries.js | 22 +- src/pages/nearle/dispatch/Dispatch.css | 947 ++++++++++++++++++++-- src/pages/nearle/dispatch/Dispatch.js | 583 +++++++++++-- src/utils/leafletPolylineOffset.js | 154 ++++ 4 files changed, 1563 insertions(+), 143 deletions(-) create mode 100644 src/utils/leafletPolylineOffset.js diff --git a/src/pages/nearle/deliveries/deliveries.js b/src/pages/nearle/deliveries/deliveries.js index 9f58c31..0c3f3cb 100644 --- a/src/pages/nearle/deliveries/deliveries.js +++ b/src/pages/nearle/deliveries/deliveries.js @@ -121,6 +121,7 @@ const Deliveries = () => { const [deliverylat, setDeliverylat] = useState(''); const [deliverylong, setDeliverylong] = useState(''); const [currentStatus, setCurrentStatus] = useState('pending'); + const [updateStatus, setUpdateStatus] = useState('delivered'); const locationRef = useRef(null); const tenantRef = useRef(null); const [page, setPage] = React.useState(0); @@ -1259,6 +1260,7 @@ const Deliveries = () => { setDeliverylong(selectedRow.droplon); setNotes(selectedRow.notes); setDeliveryamount(selectedRow.deliveryamount); + setUpdateStatus(selectedRow.orderstatus || 'delivered'); setCurrentorder(selectedRow); setDialogopen(true); handleMenuClose(); @@ -1657,6 +1659,24 @@ const Deliveries = () => { setDeliveryamount(+e.target.value)} /> + + Status + { + setUpdateStatus(e.target.value); + }} + > + Pending + Accepted + Started + Arrived + Delivered + Cancelled + + Notes setNotes(e.target.value)} /> @@ -1694,7 +1714,7 @@ const Deliveries = () => { updateDeliveryMutation.mutate({ deliveryid: currentorder.deliveryid, orderheaderid: currentorder.orderheaderid, - orderstatus: 'delivered', + orderstatus: updateStatus, deliverytime: dayjs().format('YYYY-MM-DD HH:mm:ss'), deliverylat, deliverylong, diff --git a/src/pages/nearle/dispatch/Dispatch.css b/src/pages/nearle/dispatch/Dispatch.css index bef0ca7..e114bc5 100644 --- a/src/pages/nearle/dispatch/Dispatch.css +++ b/src/pages/nearle/dispatch/Dispatch.css @@ -55,7 +55,7 @@ padding: 0 24px; background: var(--bg); border-bottom: 1px solid var(--border); - z-index: 10; + z-index: 1010; } .dispatch-container .logo { @@ -244,8 +244,8 @@ font-weight: 600; font-family: 'JetBrains Mono', monospace; background: var(--bg-sub); - padding: 6px 14px; - border-radius: 8px; + padding: 7px 16px; + border-radius: 10px; border: 1px solid var(--border); } @@ -255,9 +255,9 @@ .dispatch-container .hdr-stats { display: flex; align-items: center; - gap: 10px; + gap: 16px; margin-left: auto; - margin-right: 12px; + margin-right: 16px; min-width: 0; flex-wrap: nowrap; } @@ -349,8 +349,8 @@ .dispatch-container .strat-stat { display: inline-flex; align-items: center; - gap: 6px; - padding: 6px 12px; + gap: 8px; + padding: 7px 15px; border-radius: 999px; font-size: 12px; font-weight: 700; @@ -420,11 +420,11 @@ .dispatch-container .live-status { display: inline-flex; align-items: center; - gap: 6px; + gap: 8px; font-size: 12px; font-weight: 600; color: var(--text-muted); - padding: 6px 10px; + padding: 7px 15px; border-radius: 999px; background: var(--bg-sub); border: 1px solid var(--border); @@ -456,34 +456,416 @@ 50% { opacity: 0.4; transform: scale(0.85); } } -.dispatch-container .live-date-label { +/* ── Date picker chip ───────────────────────────────────────────── + Three-part pill: prev-day arrow ◂ | formatted-date card | ▸ next-day + arrow. The center card overlays a transparent native + so clicking anywhere on the chip opens the OS date dialog while still + showing a glanceable formatted value (`Mon, May 25, 2026`). A small + "Today" badge appears when the picked date matches today, and the + next-day arrow disables itself there. + + Design language: matches the Compare-button family — soft white card, + indigo border + halo on hover/focus, subtle lift on interaction. + ──────────────────────────────────────────────────────────────── */ +.dispatch-container .date-chip { + position: relative; /* anchors .date-cal-popover */ + display: inline-flex; + align-items: stretch; + gap: 0; + background: #ffffff; + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 12px; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), + 0 4px 12px rgba(15, 23, 42, 0.06); + transition: border-color 0.18s ease, box-shadow 0.18s ease, + transform 0.18s ease; +} + +.dispatch-container .date-chip.is-open { + border-color: #6366f1; + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2), + 0 12px 30px rgba(99, 102, 241, 0.22); +} + +.dispatch-container .date-chip:hover { + border-color: rgba(99, 102, 241, 0.45); + box-shadow: 0 2px 4px rgba(15, 23, 42, 0.06), + 0 8px 22px rgba(99, 102, 241, 0.15); + transform: translateY(-1px); +} + +.dispatch-container .date-chip:focus-within { + border-color: #6366f1; + box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2), + 0 8px 22px rgba(99, 102, 241, 0.22); +} + +/* Center card — visible chrome the operator reads. Renders as a + + + + {datePickerOpen && ( +
+ {/* Month header — month/year title flanked by + prev/next arrows. The next-month arrow disables + once we'd cross into a purely-future month so + the operator never lands on a month they can't + pick a date in. */} +
+ +
+ {calViewMonth.format('MMMM YYYY')} +
+ +
+ +
+ {WEEKDAYS.map((w) => ( +
{w}
+ ))} +
+ +
+ {cells.map((d) => { + const inMonth = d.month() === calViewMonth.month(); + const isSel = d.format('YYYY-MM-DD') === selectedDate; + const isTodayCell = d.format('YYYY-MM-DD') === todayStr; + const disabled = d.isAfter(today, 'day'); + const cls = [ + 'date-cal-day', + !inMonth && 'is-other-month', + isSel && 'is-selected', + isTodayCell && 'is-today', + disabled && 'is-disabled' + ].filter(Boolean).join(' '); + return ( + + ); + })} +
+ + {/* Quick presets — the three dates ops scrub to + most often. Saves a month-nav + a day-click for + the common cases. */} +
+ + + +
+
+ )} + + ); + })()} )} @@ -2604,7 +2922,7 @@ const Dispatch = ({
Slot timings
-
Hours are 0–24 (24h clock). Start < End.
+
Hours are 0–24 (24h clock). Half-hour steps allowed (e.g. 12.5 = 12:30). Start < End.
{slotsConfig.map((s, idx) => ( @@ -2615,11 +2933,16 @@ const Dispatch = ({ { - const v = Math.max(0, Math.min(23, parseInt(e.target.value, 10) || 0)); + // Half-hour-aware: parseFloat + snap to nearest 0.5 + // so 12.5 (12:30) is a valid value and odd inputs + // like 12.7 round to 12.5. + const raw = parseFloat(e.target.value); + const snapped = Number.isFinite(raw) ? Math.round(raw * 2) / 2 : 0; + const v = Math.max(0, Math.min(23.5, snapped)); setSlotsConfig((cur) => cur.map((row, i) => i === idx ? { ...row, startHour: v, label: formatSlotLabel(i, v), range: formatSlotRange(v, row.endHour) } @@ -2632,12 +2955,14 @@ const Dispatch = ({ End { - const v = Math.max(1, Math.min(24, parseInt(e.target.value, 10) || 1)); + const raw = parseFloat(e.target.value); + const snapped = Number.isFinite(raw) ? Math.round(raw * 2) / 2 : 0.5; + const v = Math.max(0.5, Math.min(24, snapped)); setSlotsConfig((cur) => cur.map((row, i) => i === idx ? { ...row, endHour: v, range: formatSlotRange(row.startHour, v) } @@ -3606,12 +3931,41 @@ const Dispatch = ({ mouseout: (e) => e.target.closePopup() }} > - -
RIDER
-
{p.riderName}
-
Progress{p.completedCount} / {p.totalCount} delivered
-
Next stop#{p.nextStep} · {p.nextCustomer || '—'}
-
Position{onRoad ? 'on road' : 'estimating…'}
+ +
+
+ RIDER ROUTE +
+
+
+
+ +
+
+
{p.riderName}
+
Active route details
+
+
+
+
+ Progress + + {p.completedCount} / {p.totalCount} delivered + +
+
+ Next Stop + + #{p.nextStep} · {p.nextCustomer || '—'} + +
+
+ Position + + {onRoad ? 'On road' : 'Estimating…'} + +
+
); @@ -3657,14 +4011,60 @@ const Dispatch = ({ mouseout: (e) => e.target.closePopup() }} > - -
LIVE GPS
-
{r.username}
-
Status{r.status || 'unknown'}
- {r.orderid &&
Order#{r.orderid}
} - {r.contactno &&
Phone{r.contactno}
} - {r.logdate &&
Last seen{r.logdate}
} -
Position{r.lat.toFixed(5)}, {r.lon.toFixed(5)}
+ +
+
+ + + + LIVE GPS +
+ {r.status && ( + + {r.status} + + )} +
+
+
+ +
+
+
{r.username || `Rider #${r.id}`}
+
Rider ID: #{r.id}
+
+
+
+ {r.orderid && ( +
+ Active Order + #{r.orderid} +
+ )} + {r.contactno && ( + + )} + {r.logdate && ( +
+ Last Seen + + {' '} + {dayjs(r.logdate).isValid() ? dayjs(r.logdate).format('hh:mm:ss A') : r.logdate} + +
+ )} +
+ Position + + {r.lat.toFixed(5)}, {r.lon.toFixed(5)} + +
+
); @@ -3679,7 +4079,17 @@ const Dispatch = ({ so the operator can read overlap at a glance. */} {compareOpen && focusedRider && compareViewMode !== 'planned' && (riderActualTracks.map((t, i) => { if (t.coords.length === 0) return null; + // `color` drives the drop pin, start pin, and tooltip header so + // those keep their per-step palette identity (the same colors + // the timeline uses). `polylineColor` is what the GPS line + // itself draws with — collapsed to a single emerald in Combined + // view so the actual layer reads as one cohesive trail next to + // the indigo planned rail; Actual-only mode keeps step palette + // on the polyline since there's no second layer to confuse with. const color = stepColor(i); + const polylineColor = compareViewMode === 'combined' + ? COMBINED_ACTUAL_COLOR + : color; const startPos = [t.coords[0].lat, t.coords[0].lng]; const endPos = [t.coords[t.coords.length - 1].lat, t.coords[t.coords.length - 1].lng]; const snapped = osrmTrackRoutes[t.deliveryid]; @@ -3778,6 +4188,15 @@ const Dispatch = ({ } }; + // Combined view rail-offset (negative side): mirror image of + // the +5px shift applied to the planned polyline in + // renderRoutes. With planned at +5 and actual at -5, the two + // layers sit as parallel rails ~10px apart when they share + // a road, so the operator can read both even on tight match. + // Actual-only and Planned-only modes leave offset = 0 since + // there's only one layer drawing on the map. + const actualOffset = compareViewMode === 'combined' ? -5 : 0; + return ( {drawPolyline && ( @@ -3788,7 +4207,8 @@ const Dispatch = ({ weight: isFocusedStep ? 11 : 9, opacity: isFocusedStep ? 0.75 : 0.55, lineJoin: 'round', - lineCap: 'round' + lineCap: 'round', + offset: actualOffset }} /> )} @@ -3796,11 +4216,12 @@ const Dispatch = ({ )} @@ -4251,27 +4672,33 @@ const Dispatch = ({ what each line/marker means. Lives in the header so it doesn't compete with the map for vertical real estate. */} {(() => { - // Both planned and actual share the same per-step palette - // — they're separated by stroke style on the unified map - // (planned = dashed, actual = solid). The swatch shows - // the focused step's color when one is focused, or a - // multi-hue gradient ("varies by step") when in overall. - const swatchBg = focusedDelta + // Two color stories depending on view mode: + // • Combined: polylines collapse to fixed indigo (planned) + // and emerald (actual) so the two overlaid layers can be + // told apart at a glance. Legend swatches mirror this. + // • Planned-only / Actual-only: single layer on the map, + // so polylines keep STEP_PALETTE and the swatch shows + // the focused step's color or a step-gradient strip + // (signals "varies by step"). + const isCombined = compareViewMode === 'combined'; + const stepSwatchBg = focusedDelta ? stepColor(focusedDelta.sequenceStep - 1) : `linear-gradient(90deg, ${STEP_PALETTE.slice(0, 6).join(', ')})`; + const plannedSwatchBg = isCombined ? COMBINED_PLANNED_COLOR : stepSwatchBg; + const actualSwatchBg = isCombined ? COMBINED_ACTUAL_COLOR : stepSwatchBg; return (
Planned (dashed) Actual GPS (solid) diff --git a/src/utils/leafletPolylineOffset.js b/src/utils/leafletPolylineOffset.js new file mode 100644 index 0000000..2e386e8 --- /dev/null +++ b/src/utils/leafletPolylineOffset.js @@ -0,0 +1,154 @@ +// Vendored from leaflet-polylineoffset@1.1.1 (MIT). +// +// Why this lives in-tree instead of being an npm dep: +// • The published package would require --legacy-peer-deps because of an +// unrelated React-17 peer-dep conflict elsewhere in the project, and we +// don't want a renderer plugin to force a global resolver flag. +// • It's frozen upstream (no meaningful updates since 2020), tiny, and +// has zero runtime deps besides leaflet (already in package.json). +// +// What it does: +// Monkey-patches L.Polyline so that any path passed with a numeric +// `offset` in pathOptions is rendered shifted perpendicular to its +// direction of travel by that many pixels (positive = right of travel, +// negative = left). Used by Dispatch.js's Compare → Combined view to +// render planned + actual as parallel rails when they share the same +// road geometry; without this they overlap and read as one polyline. +// +// Import once for the side effect: +// import '../../../utils/leafletPolylineOffset'; +// +// Then add to any pathOptions: +// pathOptions={{ ..., offset: 5 }} +// +// Plays nicely with both SVG and Canvas renderers. + +import L from 'leaflet'; + +L.PolylineOffset = { + translatePoint(pt, dist, radians) { + return L.point(pt.x + dist * Math.cos(radians), pt.y + dist * Math.sin(radians)); + }, + + offsetPointLine(points, distance) { + const l = points.length; + if (l < 2) { + throw new Error('Line should be defined by at least 2 points'); + } + let a = points[0]; + let b; + const offsetAngle = Math.PI / 2; + const offsetSegments = []; + + for (let i = 1; i < l; i++) { + b = points[i]; + // Each segment's offset angle is perpendicular to its direction. + const segAngle = Math.atan2(b.y - a.y, b.x - a.x); + offsetSegments.push({ + offsetAngle: segAngle - offsetAngle, + original: [a, b], + offset: [ + this.translatePoint(a, distance, segAngle - offsetAngle), + this.translatePoint(b, distance, segAngle - offsetAngle) + ] + }); + a = b; + } + + return offsetSegments; + }, + + // Find the intersection of two segments by extending them to infinity + // along their direction, then walking along segment 1 by parameter t. + // Returns null when the segments are parallel (no intersection). + intersection(l1a, l1b, l2a, l2b) { + const line1 = this.segmentAsVector(l1a, l1b); + const line2 = this.segmentAsVector(l2a, l2b); + + const denom = -line2.x * line1.y + line1.x * line2.y; + if (denom === 0) return null; + + const s = (-line1.y * (l1a.x - l2a.x) + line1.x * (l1a.y - l2a.y)) / denom; + const t = (line2.x * (l1a.y - l2a.y) - line2.y * (l1a.x - l2a.x)) / denom; + + if (s >= 0 && s <= 1 && t >= 0 && t <= 1) { + return L.point(l1a.x + t * line1.x, l1a.y + t * line1.y); + } + return null; + }, + + segmentAsVector(a, b) { + return L.point(b.x - a.x, b.y - a.y); + }, + + // Walk the offset segments and join adjacent ones at their intersection + // points (mitered corners). When two consecutive segments don't intersect + // within their bounds (sharp turn, or co-linear), fall back to the offset + // endpoint so the polyline doesn't gap. + joinLineSegments(segments) { + const joined = []; + let last = segments[0].offset; + joined.push(last[0]); + + for (let i = 1; i < segments.length; i++) { + const next = segments[i].offset; + const inter = this.intersection(last[0], last[1], next[0], next[1]); + if (inter) { + joined.push(inter); + } else { + joined.push(last[1]); + } + last = next; + } + joined.push(last[1]); + return joined; + }, + + offsetPoints(points, offset) { + if (!points || points.length < 2) return points; + const offsets = this.offsetPointLine(points, offset); + return this.joinLineSegments(offsets); + }, + + // Operates on a ring of LatLngs by projecting → offsetting → unprojecting, + // since leaflet polyline math is in screen pixels but our points are LatLng. + offsetLatLngs(map, latlngs, offset) { + const points = latlngs.map((ll) => map.latLngToLayerPoint(ll)); + const offsetPts = this.offsetPoints(points, offset); + return offsetPts.map((p) => map.layerPointToLatLng(p)); + } +}; + +// Patch Polyline._projectLatlngs (used by both SVG and Canvas renderers) so +// that when an offset is set, the projected ring is offset before clipping. +// We keep the original on _projectLatlngsOriginal so we can call through. +const originalProject = L.Polyline.prototype._projectLatlngs; + +L.Polyline.prototype._projectLatlngs = function patchedProject(latlngs, result, projectedBounds) { + const offset = this.options.offset; + if (!offset || typeof offset !== 'number') { + return originalProject.call(this, latlngs, result, projectedBounds); + } + + // Recurse for multi-ring polylines (shouldn't happen for simple lines, + // but the leaflet API allows it). + const flat = latlngs[0] instanceof L.LatLng; + if (!flat) { + for (let i = 0; i < latlngs.length; i++) { + this._projectLatlngs(latlngs[i], result, projectedBounds); + } + return undefined; + } + + const projected = latlngs.map((ll) => this._map.latLngToLayerPoint(ll)); + const offsetted = L.PolylineOffset.offsetPoints(projected, offset); + // Update projectedBounds with each offset point so the renderer's + // viewport-clipping check still works. + for (let i = 0; i < offsetted.length; i++) { + projectedBounds.extend(offsetted[i]); + } + result.push(offsetted); + return undefined; +}; + +export default L;