new chnages in ui

This commit is contained in:
2026-05-27 15:22:45 +05:30
parent 8c2248974e
commit 15f15958e6
4 changed files with 1206 additions and 456 deletions

View File

@@ -1959,6 +1959,16 @@
padding: 4px 10px; padding: 4px 10px;
border-radius: 8px; border-radius: 8px;
background: var(--bg-sub); 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 { .dispatch-container .bar-bg {
@@ -5453,7 +5463,7 @@
border-radius: 14px; border-radius: 14px;
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.18); box-shadow: 0 18px 40px rgba(15, 23, 42, 0.18);
overflow: hidden; overflow: hidden;
min-width: 460px; min-width: 580px;
animation: dispatch-popup-in 0.18s cubic-bezier(0.4, 0, 0.2, 1); 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 { .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 --- */ /* --- 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 we constrain (via leaflet's maxWidth prop) so the body grows downward as
needed for the timeline + details to render in full. */ needed for the timeline + details to render in full. */
.dispatch-container .dispatch-popup .pu-body { .dispatch-container .dispatch-popup .pu-body {
padding: 4px 18px 16px; padding: 4px 16px 12px;
} }
.dispatch-container .dispatch-popup .pu-section { .dispatch-container .dispatch-popup .pu-section {
margin-top: 12px; margin-top: 8px;
} }
.dispatch-container .dispatch-popup .pu-section-label { .dispatch-container .dispatch-popup .pu-section-label {
/* Scoped override: no horizontal margin since pu-body already provides /* Scoped override: no horizontal margin since pu-body already provides
the gutter. Sits flush with section content. */ the gutter. Sits flush with section content. */
margin: 0 0 8px; margin: 0 0 6px;
padding-bottom: 6px; padding-bottom: 4px;
font-size: 10px; font-size: 10px;
font-weight: 800; font-weight: 800;
letter-spacing: 0.08em; letter-spacing: 0.08em;
@@ -5579,18 +5659,21 @@
border-bottom: 1px solid rgba(123, 31, 162, 0.18); border-bottom: 1px solid rgba(123, 31, 162, 0.18);
} }
/* --- Timeline (scoped override of the earlier rules so paddings match /* --- Timeline: lay events out as a 2-column grid so the 6-row vertical
the new pu-body gutter) --- */ 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 { .dispatch-container .dispatch-popup .pu-timeline {
padding: 4px 0 4px 4px; padding: 2px 0;
display: flex; display: grid;
flex-direction: column; grid-template-columns: 1fr 1fr;
gap: 6px; column-gap: 14px;
row-gap: 4px;
position: relative; position: relative;
} }
.dispatch-container .dispatch-popup .pu-timeline::before { .dispatch-container .dispatch-popup .pu-timeline::before {
left: 7px; display: none;
} }
/* --- Details grid: 2 columns of icon/label/value tiles --- */ /* --- Details grid: 2 columns of icon/label/value tiles --- */
@@ -5657,8 +5740,8 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 6px; gap: 6px;
margin-top: 10px; margin-top: 8px;
padding-top: 10px; padding-top: 8px;
border-top: 1px dashed rgba(123, 31, 162, 0.18); border-top: 1px dashed rgba(123, 31, 162, 0.18);
} }

View File

@@ -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 pickupLon = (o) => o.pickuplong || o.pickuplongitude || o.picklongitude || o.pickup_lon;
const hasValidPickup = (o) => Number.isFinite(toNum(pickupLat(o))) && Number.isFinite(toNum(pickupLon(o))); 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 // Each entry covers a half-open range [startHour, endHour) measured in
// FRACTIONAL hours (e.g. 12.5 = 12:30). Half-hour boundaries are supported // 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. // Three named batches, bucketed by assigntime per spec:
// Slot 5 ends at 24 so anything from 8 PM until midnight buckets there. // • Morning Batch: before 8 AM (00:00 → 08:00)
// Default slot layout. Used as the seed for the editable slot config the // • Afternoon Batch: 9 AM → 12 PM (09:00 → 12:00)
// operator can tweak at runtime — see slotsConfig state + the slot-edit // • Evening Batch: 4 PM → 7 PM (16:00 → 19:00)
// popover below. Don't read BATCHES_DEFAULT directly at runtime; read // Gaps (89 AM, 12 PM4 PM, 7 PM+) intentionally fall outside every batch.
// 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)
const BATCHES_DEFAULT_RAW = [ const BATCHES_DEFAULT_RAW = [
{ id: 'slot-1', startHour: 8, endHour: 12.5 }, { id: 'morning', name: 'Morning Batch', startHour: 0, endHour: 8 },
{ id: 'slot-2', startHour: 12 + 20 / 60, endHour: 15 }, { id: 'afternoon', name: 'Afternoon Batch', startHour: 9, endHour: 12 },
{ id: 'slot-3', startHour: 15, endHour: 19 }, { id: 'evening', name: 'Evening Batch', startHour: 16, endHour: 19 }
{ id: 'slot-4', startHour: 19, endHour: 20 },
{ id: 'slot-5', startHour: 20, endHour: 24 }
]; ];
// v6: the five-named-wave layout with validation checks for array lengths. // v7: three-named-batch layout (Morning / Afternoon / Evening).
// Bumping the key drops cached layouts from v5 and earlier in favour of the new defaults. // Bumping the key drops cached 5-slot layouts from v6 and earlier.
const SLOTS_STORAGE_KEY = 'dispatch.slots.v6'; const SLOTS_STORAGE_KEY = 'dispatch.slots.v7';
// Every prior storage key. Wiped once on mount so stale layouts // Every prior storage key. Wiped once on mount so stale layouts
// from earlier code versions can't reappear on the next page load. // 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.v2',
'dispatch.slots.v3', 'dispatch.slots.v3',
'dispatch.slots.v4', '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 // 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" // 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 // etc.) guarantees user-edited slots and default slots render the same way
// — no chance of drift between the two paths. // — 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) => ({ const BATCHES_DEFAULT = BATCHES_DEFAULT_RAW.map((s, i) => ({
...s, ...s,
label: formatSlotLabel(i, s.startHour), label: s.name || formatSlotLabel(i, s.startHour),
range: formatSlotRange(s.startHour, s.endHour) range: formatSlotRange(s.startHour, s.endHour)
})); }));
@@ -196,12 +191,14 @@ const getBatchForHour = (h, batches) => {
// timestamp `getRowBatch` reads. "Delivery" defaults to actual deliverytime // timestamp `getRowBatch` reads. "Delivery" defaults to actual deliverytime
// with a fallback to expecteddeliverytime so undelivered orders still bucket. // with a fallback to expecteddeliverytime so undelivered orders still bucket.
const TIME_FIELDS = [ 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: 'assigned', label: 'Assigned', keys: ['assigntime'] },
{ id: 'accepted', label: 'Accepted', keys: ['acceptedtime'] }, { id: 'accepted', label: 'Accepted', keys: ['acceptedtime'] },
{ id: 'started', label: 'Started', keys: ['starttime'] }, { id: 'started', label: 'Started', keys: ['starttime'] },
{ id: 'arrived', label: 'Arrived', keys: ['arrivaltime'] }, { 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) => { const getTimeFieldValue = (r, fieldId) => {
@@ -212,7 +209,7 @@ const getTimeFieldValue = (r, fieldId) => {
return null; return null;
}; };
const getRowBatch = (r, fieldId = 'delivery', batches = BATCHES_DEFAULT) => { const getRowBatch = (r, fieldId = 'all', batches = BATCHES_DEFAULT) => {
const t = getTimeFieldValue(r, fieldId); const t = getTimeFieldValue(r, fieldId);
if (!t) return null; if (!t) return null;
const str = String(t).trim(); const str = String(t).trim();
@@ -548,6 +545,22 @@ L.Icon.Default.mergeOptions({
const RIDER_COLORS = ['#0055FF', '#00D82C', '#FF6B00', '#9D00FF', '#FF00A8', '#00C2B2', '#FF9900', '#FF0000']; 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, // STATUS_STYLES, getStatusStyle, FINAL_STATUSES, SKIPPED_STATUSES,
// STEP_PALETTE, stepColor — moved to ./dispatchShared.js so the // STEP_PALETTE, stepColor — moved to ./dispatchShared.js so the
// extracted CompareDataPanel component can import them without forcing // extracted CompareDataPanel component can import them without forcing
@@ -715,6 +728,13 @@ const Dispatch = ({
// or vice versa without immediately triggering a close. // or vice versa without immediately triggering a close.
const activePopupMarkerRef = useRef(null); const activePopupMarkerRef = useRef(null);
const popupHoverTimerRef = 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 <Popup>: 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 isControlled = selectedRiderId !== undefined;
const [clock, setClock] = useState(''); const [clock, setClock] = useState('');
@@ -736,11 +756,11 @@ const Dispatch = ({
const [locationMenuOpen, setLocationMenuOpen] = useState(false); const [locationMenuOpen, setLocationMenuOpen] = useState(false);
const locationMenuRef = useRef(null); const locationMenuRef = useRef(null);
// Which timestamp column drives slot bucketing. Default = delivery time // Which timestamp column drives slot bucketing. Default = assigntime so
// (operator's primary mental model — "did this order land in the X-Y wave?"). // orders bucket into Morning/Afternoon/Evening by when they were assigned,
// Switching to Assigned/Accepted/Arrived/Pickup/Started rebuckets every row // per current spec. The status-wise time-field dropdown is hidden for now
// through `getRowBatch(_, selectedTimeField)`. // (see commented-out block in JSX), so this stays fixed at 'assigned'.
const [selectedTimeField, setSelectedTimeField] = useState('delivery'); const [selectedTimeField, setSelectedTimeField] = useState('assigned');
const [timeFieldMenuOpen, setTimeFieldMenuOpen] = useState(false); const [timeFieldMenuOpen, setTimeFieldMenuOpen] = useState(false);
const timeFieldMenuRef = useRef(null); const timeFieldMenuRef = useRef(null);
@@ -755,18 +775,27 @@ const Dispatch = ({
const raw = window.localStorage.getItem(SLOTS_STORAGE_KEY); const raw = window.localStorage.getItem(SLOTS_STORAGE_KEY);
if (!raw) return BATCHES_DEFAULT; if (!raw) return BATCHES_DEFAULT;
const parsed = JSON.parse(raw); const parsed = JSON.parse(raw);
// If the parsed slots length does not match BATCHES_DEFAULT_RAW, the data is stale // If the parsed slots length does not match BATCHES_DEFAULT_RAW, the data
// (e.g. written from a 3-slot layout version during hot reload). Discard it and load default 5 slots. // 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; 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 // 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. // formatter (e.g. AM/PM style) flow through to old persisted slots. If
return parsed.map((s, i) => ({ // the saved id matches a default batch, prefer that batch's friendly
id: s.id || `slot-${i + 1}`, // name ("Morning Batch", etc.) over the generated "Slot N · time" label.
startHour: Number(s.startHour) || 0, return parsed.map((s, i) => {
endHour: Number(s.endHour) || 24, const id = s.id || `slot-${i + 1}`;
label: formatSlotLabel(i, Number(s.startHour) || 0), const startHour = Number(s.startHour) || 0;
range: formatSlotRange(Number(s.startHour) || 0, Number(s.endHour) || 24) 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) { } catch (e) {
return BATCHES_DEFAULT; return BATCHES_DEFAULT;
} }
@@ -1956,16 +1985,20 @@ const Dispatch = ({
}, [selectedBatch]); }, [selectedBatch]);
// When the user clicks a step in the focused-rider sidebar (sets focusedStop), // 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. // surface that order in the centered popup overlay so the details show up
// Wait one frame so MapController has a chance to recenter first. // 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(() => { useEffect(() => {
if (!focusedStop) return; if (!focusedStop) return;
const t = setTimeout(() => { const t = setTimeout(() => {
const marker = orderMarkerRefs.current[String(focusedStop.orderid)]; const fullOrder = allOrders?.find?.((o) => String(o.orderid) === String(focusedStop.orderid));
if (marker && typeof marker.openPopup === 'function') marker.openPopup(); if (fullOrder) setCenterPopupOrder(fullOrder);
}, 350); }, 350);
return () => clearTimeout(t); return () => clearTimeout(t);
}, [focusedStop]); }, [focusedStop, allOrders]);
const startAnimation = () => { const startAnimation = () => {
if (isAnimating) { if (isAnimating) {
@@ -2152,7 +2185,13 @@ const Dispatch = ({
const getRiderColor = (rid) => riders.find(r => r.id === rid)?.color || '#475569'; 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. // Shared rider-card markup, used in the "By Rider" panel and inside the focused-zone detail.
const renderRiderCard = (r, i) => ( 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 (
<div key={r.id} className="rcard" onClick={() => handleRiderFocus(r)} style={{ animationDelay: `${i * 0.05}s` }}> <div key={r.id} className="rcard" onClick={() => handleRiderFocus(r)} style={{ animationDelay: `${i * 0.05}s` }}>
<div className="rcard-top"> <div className="rcard-top">
<div className="rcard-emo" style={{ background: `${r.color}18`, borderColor: `${r.color}50`, color: r.color }}><MdTwoWheeler /></div> <div className="rcard-emo" style={{ background: `${r.color}18`, borderColor: `${r.color}50`, color: r.color }}><MdTwoWheeler /></div>
@@ -2160,24 +2199,29 @@ const Dispatch = ({
<div className="rcard-name">{r.riderName}</div> <div className="rcard-name">{r.riderName}</div>
<div className="rcard-zone">{r.orders[0]?.zone_name || locationName || 'Local'} · {new Set(r.orders.map(o => o.trip_number || 1)).size} trips</div> <div className="rcard-zone">{r.orders[0]?.zone_name || locationName || 'Local'} · {new Set(r.orders.map(o => o.trip_number || 1)).size} trips</div>
</div> </div>
<div className="rcard-badge" style={{ background: `${r.color}18`, color: r.color }}>{r.orders.length}</div> <div
className={`rcard-badge ${isDone ? 'is-done' : ''}`}
style={isDone ? undefined : { background: `${r.color}18`, color: r.color }}
title={`${delivered} delivered of ${total} total`}
>
{delivered}/{total}
</div> </div>
<div className="bar-bg"><div className="bar-fg" style={{ width: `${Math.min(100, (r.orders.length / 15) * 100)}%`, background: r.color }}></div></div> </div>
<div className="bar-bg"><div className="bar-fg" style={{ width: `${Math.min(100, (total / 15) * 100)}%`, background: r.color }}></div></div>
<div className="rcard-meta"><span><Ico><MdStraighten /></Ico>{r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0).toFixed(1)} km</span><span><Ico><MdAccountBalanceWallet /></Ico>{r.orders.reduce((s, o) => s + parseFloat(o.profit || 0), 0).toFixed(0)}</span></div> <div className="rcard-meta"><span><Ico><MdStraighten /></Ico>{r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0).toFixed(1)} km</span><span><Ico><MdAccountBalanceWallet /></Ico>{r.orders.reduce((s, o) => s + parseFloat(o.profit || 0), 0).toFixed(0)}</span></div>
<div className="step-ids"> <div className="step-ids">
{r.orders.slice(0, 15).map(o => <span key={o.orderid} className="step-id">S{o.step}</span>)} {r.orders.slice(0, 15).map(o => <span key={o.orderid} className="step-id">S{o.step}</span>)}
</div> </div>
</div> </div>
); );
};
// Shared order-popup body used by both the planned-route number markers // Returns true when the order's centered popup should stay open even after
// and the Compare mode actual-track drop pins. Extracted so clicking a // the cursor leaves the marker: either explicitly pinned via click, or the
// pin on either layer surfaces the exact same Timeline + Details + KM // matching compare-step is focused (so clicking a step in the right panel
// chips — operators learn the layout once and trust it everywhere. // keeps the card visible while the maps recenter).
const renderOrderPopupContent = (o) => { const isOrderPopupPinned = (o) => {
const statusStyle = getStatusStyle(o.orderstatus); if (!o) return false;
const isPinned = () => {
if (pinnedPopupsRef.current.has(String(o.orderid))) return true; if (pinnedPopupsRef.current.has(String(o.orderid))) return true;
if (compareOpen && focusedRider && o.deliveryid != null) { if (compareOpen && focusedRider && o.deliveryid != null) {
const track = riderActualTracks.find((t) => String(t.deliveryid) === String(o.deliveryid)); const track = riderActualTracks.find((t) => String(t.deliveryid) === String(o.deliveryid));
@@ -2186,34 +2230,17 @@ const Dispatch = ({
return false; return false;
}; };
const handleMouseEnter = () => { // Shared order-popup body used by both the planned-route number markers
if (popupHoverTimerRef.current) { // and the Compare mode actual-track drop pins. Extracted so clicking a
clearTimeout(popupHoverTimerRef.current); // pin on either layer surfaces the exact same Timeline + Details + KM
popupHoverTimerRef.current = null; // 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 handleMouseLeave = () => { const statusStyle = getStatusStyle(o.orderstatus);
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 ( return (
<div <div style={{ height: '100%', width: '100%' }}>
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{ height: '100%', width: '100%' }}
>
<div className="pu-header"> <div className="pu-header">
<div className="pu-header-top"> <div className="pu-header-top">
<div className="pu-id">ORDER #{o.orderid}</div> <div className="pu-id">ORDER #{o.orderid}</div>
@@ -2254,21 +2281,31 @@ const Dispatch = ({
<div className="pu-section"> <div className="pu-section">
<div className="pu-section-label">Details</div> <div className="pu-section-label">Details</div>
<div className="pu-details-grid"> <div className="pu-details-grid">
{o.pickupcustomer && ( {(o.pickupcustomer || o.locationname || o.pickuplocation) && (
<div className="pu-detail"> <div className="pu-detail">
<div className="pu-detail-icon"><MdRestaurant /></div> <div className="pu-detail-icon"><MdRestaurant /></div>
<div className="pu-detail-body"> <div className="pu-detail-body">
<div className="pu-detail-label">Kitchen</div> <div className="pu-detail-label">Pickup</div>
<div className="pu-detail-value" title={o.pickupcustomer}>{o.pickupcustomer}</div> <div
className="pu-detail-value"
title={o.pickupcustomer || o.locationname || o.pickuplocation}
>
{o.pickupcustomer || o.locationname || o.pickuplocation}
</div>
</div> </div>
</div> </div>
)} )}
{(o.locationname || o.pickuplocation) && ( {(o.deliverysuburb || o.deliveryaddress) && (
<div className="pu-detail"> <div className="pu-detail">
<div className="pu-detail-icon"><MdPlace /></div> <div className="pu-detail-icon"><MdPlace /></div>
<div className="pu-detail-body"> <div className="pu-detail-body">
<div className="pu-detail-label">Pickup</div> <div className="pu-detail-label">Drop</div>
<div className="pu-detail-value" title={o.locationname || o.pickuplocation}>{o.locationname || o.pickuplocation}</div> <div
className="pu-detail-value"
title={o.deliveryaddress || o.deliverysuburb}
>
{o.deliverysuburb || extractArea(o.deliveryaddress)}
</div>
</div> </div>
</div> </div>
)} )}
@@ -2403,46 +2440,37 @@ const Dispatch = ({
else delete orderMarkerRefs.current[String(o.orderid)]; else delete orderMarkerRefs.current[String(o.orderid)];
}} }}
eventHandlers={{ eventHandlers={{
mouseover: (e) => { mouseover: () => {
const marker = e.target;
if (popupHoverTimerRef.current) { if (popupHoverTimerRef.current) {
clearTimeout(popupHoverTimerRef.current); clearTimeout(popupHoverTimerRef.current);
popupHoverTimerRef.current = null; popupHoverTimerRef.current = null;
} }
activePopupMarkerRef.current = marker; setCenterPopupOrder(o);
marker.openPopup();
}, },
mouseout: (e) => { mouseout: () => {
const marker = e.target;
if (pinnedPopupsRef.current.has(String(o.orderid))) return; if (pinnedPopupsRef.current.has(String(o.orderid))) return;
if (popupHoverTimerRef.current) { if (popupHoverTimerRef.current) {
clearTimeout(popupHoverTimerRef.current); clearTimeout(popupHoverTimerRef.current);
} }
popupHoverTimerRef.current = setTimeout(() => { popupHoverTimerRef.current = setTimeout(() => {
marker.closePopup(); setCenterPopupOrder((cur) =>
if (activePopupMarkerRef.current === marker) { cur && String(cur.orderid) === String(o.orderid) ? null : cur
activePopupMarkerRef.current = null; );
}
popupHoverTimerRef.current = null; popupHoverTimerRef.current = null;
}, 200); }, 200);
}, },
click: (e) => { click: () => {
const id = String(o.orderid); const id = String(o.orderid);
if (pinnedPopupsRef.current.has(id)) { if (pinnedPopupsRef.current.has(id)) {
pinnedPopupsRef.current.delete(id); pinnedPopupsRef.current.delete(id);
e.target.closePopup(); setCenterPopupOrder(null);
} else { } else {
pinnedPopupsRef.current.add(id); pinnedPopupsRef.current.add(id);
e.target.openPopup(); setCenterPopupOrder(o);
} }
} }
}} }}
> />
<Popup maxWidth={520} minWidth={460} className="dispatch-popup" autoPan={true} autoPanPadding={[40, 40]}>
{renderOrderPopupContent(o)}
</Popup>
</Marker>
); );
}); });
}; };
@@ -2963,11 +2991,10 @@ const Dispatch = ({
{shouldFetchLive && viewMode !== 'rider-info' && ( {shouldFetchLive && viewMode !== 'rider-info' && (
<div id="batch-row"> <div id="batch-row">
<span className="batch-label">Slot</span> <span className="batch-label">Batch</span>
{/* Dropdown to pick which timestamp drives slot bucketing. Mirrors {/* Status-wise (time-field) filter is hidden for now per spec —
the hub-location dropdown's look so it reads as the same kind of bucketing is locked to `assigntime`. Restore this block to bring
filter control. The chosen field reruns batchCounts + back the Delivered/Pending/Assigned/... dropdown.
filteredLiveRows via selectedTimeField. */}
<div className="time-field-wrap" ref={timeFieldMenuRef}> <div className="time-field-wrap" ref={timeFieldMenuRef}>
<button <button
type="button" type="button"
@@ -2978,7 +3005,7 @@ const Dispatch = ({
title="Bucket slots by this timestamp" title="Bucket slots by this timestamp"
> >
<MdAccessTime /> <MdAccessTime />
<span className="time-field-text">{TIME_FIELDS.find((f) => f.id === selectedTimeField)?.label || 'Delivery'}</span> <span className="time-field-text">{TIME_FIELDS.find((f) => f.id === selectedTimeField)?.label || 'Delivered'}</span>
<MdExpandMore className="time-field-caret" /> <MdExpandMore className="time-field-caret" />
</button> </button>
{timeFieldMenuOpen && ( {timeFieldMenuOpen && (
@@ -3006,9 +3033,11 @@ const Dispatch = ({
</div> </div>
)} )}
</div> </div>
{/* Slot editor — lets the operator tweak start/end hours, add a new */}
slot, remove an existing one, or reset to defaults. Persists via {/* Slot editor (Edit slots button + panel) is hidden for now per
SLOTS_STORAGE_KEY in localStorage. */} 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.
<div className="slot-edit-wrap" ref={slotEditRef}> <div className="slot-edit-wrap" ref={slotEditRef}>
<button <button
type="button" type="button"
@@ -3040,9 +3069,6 @@ const Dispatch = ({
step={0.5} step={0.5}
value={s.startHour} value={s.startHour}
onChange={(e) => { onChange={(e) => {
// 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 raw = parseFloat(e.target.value);
const snapped = Number.isFinite(raw) ? Math.round(raw * 2) / 2 : 0; const snapped = Number.isFinite(raw) ? Math.round(raw * 2) / 2 : 0;
const v = Math.max(0, Math.min(23.5, snapped)); const v = Math.max(0, Math.min(23.5, snapped));
@@ -3127,6 +3153,7 @@ const Dispatch = ({
</div> </div>
)} )}
</div> </div>
*/}
{/* Inner scroller — keeps the "Slot" label fixed while the chip list scrolls {/* Inner scroller — keeps the "Slot" label fixed while the chip list scrolls
horizontally when it overflows. */} horizontally when it overflows. */}
<div className="batch-scroll"> <div className="batch-scroll">
@@ -3191,7 +3218,7 @@ const Dispatch = ({
className={`ri-rider-item ${isActive ? 'active' : ''}`} className={`ri-rider-item ${isActive ? 'active' : ''}`}
onClick={() => setRiderInfoUserid(r.id)} onClick={() => setRiderInfoUserid(r.id)}
> >
<span className="ri-rider-dot" style={{ background: getRiderColor(r.id) }} /> <span className="ri-rider-dot" style={{ background: getStableRiderColor(r.id) }} />
<span className="ri-rider-info-block"> <span className="ri-rider-info-block">
<span className="ri-rider-name">{r.riderName}</span> <span className="ri-rider-name">{r.riderName}</span>
<span className="ri-rider-meta">#{r.id}</span> <span className="ri-rider-meta">#{r.id}</span>
@@ -4371,32 +4398,29 @@ const Dispatch = ({
eventHandlers={ eventHandlers={
orderForTrack orderForTrack
? { ? {
// Match the planned-route marker UX: hover opens // Match the planned-route marker UX: hover surfaces
// the rich order popup, leaving it pinned while // the rich order card in the centered overlay. The
// a step is focused (so click-to-focus keeps the
// modal visible after the cursor moves away). The
// ~200ms grace timer on mouseout lets the cursor // ~200ms grace timer on mouseout lets the cursor
// travel onto the popup itself without flicker. // travel onto the overlay without flicker. Pinning
mouseover: (e) => { // is implicit while focusedCompareStep === this
const marker = e.target; // step, so the card stays put while the user is
// inspecting this delivery.
mouseover: () => {
if (popupHoverTimerRef.current) { if (popupHoverTimerRef.current) {
clearTimeout(popupHoverTimerRef.current); clearTimeout(popupHoverTimerRef.current);
popupHoverTimerRef.current = null; popupHoverTimerRef.current = null;
} }
activePopupMarkerRef.current = marker; setCenterPopupOrder(orderForTrack);
marker.openPopup();
}, },
mouseout: (e) => { mouseout: () => {
if (focusedCompareStep === t.sequenceStep) return; if (focusedCompareStep === t.sequenceStep) return;
const marker = e.target;
if (popupHoverTimerRef.current) { if (popupHoverTimerRef.current) {
clearTimeout(popupHoverTimerRef.current); clearTimeout(popupHoverTimerRef.current);
} }
popupHoverTimerRef.current = setTimeout(() => { popupHoverTimerRef.current = setTimeout(() => {
marker.closePopup(); setCenterPopupOrder((cur) =>
if (activePopupMarkerRef.current === marker) { cur && String(cur.orderid) === String(orderForTrack.orderid) ? null : cur
activePopupMarkerRef.current = null; );
}
popupHoverTimerRef.current = null; popupHoverTimerRef.current = null;
}, 200); }, 200);
}, },
@@ -4452,17 +4476,6 @@ const Dispatch = ({
})()} })()}
</Tooltip> </Tooltip>
)} )}
{orderForTrack && (
<Popup
maxWidth={520}
minWidth={460}
className="dispatch-popup"
autoPan={true}
autoPanPadding={[40, 40]}
>
{renderOrderPopupContent(orderForTrack)}
</Popup>
)}
</Marker> </Marker>
</React.Fragment> </React.Fragment>
); );
@@ -4508,6 +4521,10 @@ const Dispatch = ({
</div> */} </div> */}
</div> </div>
{/* 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.
<div id="ov-tr"> <div id="ov-tr">
{viewMode === 'kitchens' ? ( {viewMode === 'kitchens' ? (
kitchens.slice(0, 10).map(k => { kitchens.slice(0, 10).map(k => {
@@ -4541,6 +4558,7 @@ const Dispatch = ({
}) })
)} )}
</div> </div>
*/}
<div id="ov-br"> <div id="ov-br">
<button className={`sbt ${isAnimating ? 'active' : ''}`} onClick={startAnimation} style={{ boxShadow: 'var(--shadow-lg)', background: isAnimating ? 'var(--accent)' : '#fff' }}> <button className={`sbt ${isAnimating ? 'active' : ''}`} onClick={startAnimation} style={{ boxShadow: 'var(--shadow-lg)', background: isAnimating ? 'var(--accent)' : '#fff' }}>
@@ -4852,6 +4870,48 @@ const Dispatch = ({
</div> </div>
)} )}
{/* 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 && (
<div
className="dispatch-popup-center"
role="dialog"
aria-label={`Order ${centerPopupOrder.orderid} details`}
onMouseEnter={() => {
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);
}}
>
<div className="dispatch-popup-card dispatch-popup">
<button
type="button"
className="dispatch-popup-center-close"
aria-label="Close order details"
onClick={() => {
pinnedPopupsRef.current.delete(String(centerPopupOrder.orderid));
setCenterPopupOrder(null);
}}
>
×
</button>
{renderOrderPopupContent(centerPopupOrder)}
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@@ -5,7 +5,7 @@
/* ============================================== */ /* ============================================== */
.location-panel { .location-panel {
position: relative; position: relative;
padding: 16px 18px 16px 18px; padding: 12px 14px;
border-radius: 12px; border-radius: 12px;
border: 1px solid #eef2f6; border: 1px solid #eef2f6;
background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%); background: linear-gradient(180deg, #ffffff 0%, #fbfcff 100%);
@@ -39,10 +39,10 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 12px; gap: 10px;
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: 22px; margin-bottom: 12px;
padding-bottom: 18px; padding-bottom: 10px;
border-bottom: 1px dashed #e2e8f0; border-bottom: 1px dashed #e2e8f0;
} }
@@ -154,16 +154,16 @@
} }
.lp-badge { .lp-badge {
width: 42px; width: 34px;
height: 42px; height: 34px;
border-radius: 12px; border-radius: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 18px; font-size: 15px;
color: #fff; color: #fff;
flex-shrink: 0; 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 { .pickup-panel .lp-badge {
@@ -177,25 +177,26 @@
} }
.lp-title { .lp-title {
font-size: 18px; font-size: 15px;
font-weight: 700; font-weight: 700;
color: #1e293b; color: #1e293b;
line-height: 1.2; line-height: 1.2;
} }
.lp-subtitle { .lp-subtitle {
font-size: 13.5px; font-size: 11.5px;
color: #94a3b8; color: #94a3b8;
margin-top: 3px; margin-top: 1px;
} }
.lp-action-btn { .lp-action-btn {
text-transform: none !important; text-transform: none !important;
font-weight: 600 !important; font-weight: 600 !important;
font-size: 13.5px !important; font-size: 12px !important;
border-radius: 10px !important; border-radius: 8px !important;
padding: 7px 14px !important; padding: 5px 11px !important;
letter-spacing: 0.2px !important; letter-spacing: 0.2px !important;
min-height: 30px !important;
} }
.pickup-panel .lp-action-btn { .pickup-panel .lp-action-btn {
@@ -264,12 +265,12 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 12.5px; font-size: 10.5px;
font-weight: 700; font-weight: 700;
letter-spacing: 0.8px; letter-spacing: 0.7px;
text-transform: uppercase; text-transform: uppercase;
color: #64748b; color: #64748b;
margin-bottom: 14px; margin-bottom: 8px;
} }
.field-group-caption::after { .field-group-caption::after {
@@ -723,48 +724,53 @@
/* MUI Field Sizing — Readable on large screens */ /* 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, .location-panel .MuiOutlinedInput-root,
.orders-card .MuiOutlinedInput-root { .orders-card .MuiOutlinedInput-root {
font-size: 14.5px !important; font-size: 13px !important;
border-radius: 10px !important;
} }
.location-panel .MuiOutlinedInput-input, .location-panel .MuiOutlinedInput-input,
.orders-card .MuiOutlinedInput-input { .orders-card .MuiOutlinedInput-input {
font-size: 14.5px !important; font-size: 13px !important;
padding-top: 11px !important; padding-top: 9px !important;
padding-bottom: 11px !important; padding-bottom: 9px !important;
} }
.location-panel .MuiInputLabel-root, .location-panel .MuiInputLabel-root,
.orders-card .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 */ /* When label is shrunk (floating up), keep it slightly smaller for the float effect */
.location-panel .MuiInputLabel-root.MuiInputLabel-shrink, .location-panel .MuiInputLabel-root.MuiInputLabel-shrink,
.orders-card .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) */ /* MUI helper text (validation / hints under fields) */
.location-panel .MuiFormHelperText-root, .location-panel .MuiFormHelperText-root,
.orders-card .MuiFormHelperText-root { .orders-card .MuiFormHelperText-root {
font-size: 12.5px !important; font-size: 11px !important;
margin-top: 3px !important;
} }
/* Autocomplete options dropdown */ /* Autocomplete options dropdown */
.MuiAutocomplete-popper .MuiAutocomplete-option { .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 { .orders-card .MuiTypography-h5 {
font-size: 19px !important; font-size: 15px !important;
} }
.orders-card .MuiTypography-h6 { .orders-card .MuiTypography-h6 {
font-size: 16.5px !important; font-size: 13.5px !important;
} }
.orders-card:hover { .orders-card:hover {
@@ -900,99 +906,209 @@
overflow: hidden; overflow: hidden;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
border: 1px solid #eef2f6; border: 1px solid #eef2f6;
height: 380px; height: 260px;
min-height: 380px; 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 { .pricing-summary-card {
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%) !important; background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%) !important;
border: 1px solid #eef2f6 !important; border: 1px solid #eef2f6 !important;
border-radius: 16px !important; border-radius: 14px !important;
padding: 20px !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 { .price-metric-item {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 12px 0; padding: 7px 0;
border-bottom: 1px dashed #e2e8f0; border-bottom: 1px solid #f5f7fa;
} }
.price-metric-item:last-child { .price-metric-item:last-of-type {
border-bottom: none; border-bottom: none;
padding-bottom: 4px;
} }
.price-metric-label { .price-metric-label {
font-size: 14.5px; font-size: 12.5px;
color: #475569; color: #475569;
font-weight: 500; font-weight: 500;
display: flex; display: flex;
align-items: center; 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 { .price-metric-value {
font-size: 16px; font-size: 13.5px;
font-weight: 700; font-weight: 700;
color: #1e293b; color: #1e293b;
display: inline-flex;
align-items: baseline;
gap: 2px;
white-space: nowrap;
} }
.price-metric-value.highlight { .price-metric-value.highlight {
color: #1890ff; color: #1890ff;
} }
.price-metric-unit {
font-size: 11px;
font-weight: 500;
color: #94a3b8;
margin-left: 2px;
}
.total-charge-badge { .total-charge-badge {
background: linear-gradient(135deg, rgba(24, 144, 255, 0.08) 0%, rgba(101, 56, 122, 0.08) 100%); background: linear-gradient(135deg, rgba(24, 144, 255, 0.08) 0%, rgba(101, 56, 122, 0.10) 100%);
border: 1px solid rgba(24, 144, 255, 0.15); border: 1px solid rgba(101, 56, 122, 0.18);
border-radius: 12px; border-radius: 10px;
padding: 16px; padding: 10px 14px;
display: flex; display: flex;
flex-direction: column;
align-items: center; 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 { .total-charge-label {
font-size: 13px; font-size: 11.5px;
font-weight: 700; font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.8px; letter-spacing: 0.6px;
color: #65387A; color: #65387A;
margin-bottom: 6px;
} }
.total-charge-val { .total-charge-val {
font-size: 32px; font-size: 20px;
font-weight: 800; font-weight: 800;
color: #65387A; color: #65387A;
line-height: 1.1; line-height: 1.1;
letter-spacing: -0.01em;
white-space: nowrap;
} }
/* Gradient Action Button */ /* Gradient Action Button — compact professional */
.gradient-btn-create { .gradient-btn-create {
background: linear-gradient(135deg, #1890ff 0%, #65387a 100%) !important; background: linear-gradient(135deg, #1890ff 0%, #65387a 100%) !important;
color: #ffffff !important; color: #ffffff !important;
font-weight: 600 !important; font-weight: 600 !important;
border-radius: 12px !important; font-size: 13px !important;
padding: 12px 28px !important; letter-spacing: 0.01em !important;
box-shadow: 0 8px 20px -4px rgba(24, 144, 255, 0.3) !important; text-transform: none !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !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; 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 { .gradient-btn-create:hover {
transform: translateY(-2px) !important; transform: translateY(-1px) !important;
box-shadow: 0 12px 28px -4px rgba(24, 144, 255, 0.45), 0 4px 10px rgba(101, 56, 122, 0.2) !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 { .gradient-btn-create:active {
transform: translateY(0) !important; transform: translateY(0) !important;
filter: brightness(0.98);
} }
.gradient-btn-create.Mui-disabled,
.gradient-btn-create:disabled { .gradient-btn-create:disabled {
background: #cbd5e1 !important; background: #e2e8f0 !important;
color: #94a3b8 !important; color: #94a3b8 !important;
box-shadow: none !important; box-shadow: none !important;
cursor: not-allowed !important; cursor: not-allowed !important;
@@ -1261,8 +1377,8 @@
} }
.map-preview-wrapper { .map-preview-wrapper {
height: 300px; height: 220px;
min-height: 300px; min-height: 220px;
} }
.weight-card-btn { .weight-card-btn {
@@ -1300,3 +1416,463 @@
padding: 16px !important; 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%;
}
}

View File

@@ -26,7 +26,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { Empty } from 'antd'; 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 { GiDoorHandle } from 'react-icons/gi';
import { FaLandmarkDome } from 'react-icons/fa6'; import { FaLandmarkDome } from 'react-icons/fa6';
import ClearIcon from '@mui/icons-material/Clear'; 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]); }, [startPoint.latitude, startPoint.longitude, endPoint.latitude, endPoint.longitude, hasPick, hasDrop]);
return ( return (
<div style={{ position: 'relative', width: '100%', height: '100%', minHeight: '350px' }}> <div style={{ position: 'relative', width: '100%', height: '100%' }}>
<MapContainer <MapContainer
center={defaultCenter} center={defaultCenter}
zoom={12} zoom={12}
style={{ width: '100%', height: '100%', minHeight: '350px' }} style={{ width: '100%', height: '100%' }}
zoomControl={true} zoomControl={true}
> >
<TileLayer <TileLayer
@@ -257,6 +257,15 @@ const Createorder1 = () => {
const [locationValue, setLocationValue] = useState(null); const [locationValue, setLocationValue] = useState(null);
const [pickupSlotsList, setPickupSlotsList] = useState(null); const [pickupSlotsList, setPickupSlotsList] = useState(null);
const [pickupSlot, setPickupSlot] = 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(() => { useEffect(() => {
console.log('pickupSlotsList', pickupSlotsList); console.log('pickupSlotsList', pickupSlotsList);
@@ -1060,14 +1069,14 @@ const Createorder1 = () => {
<Card <Card
className="orders-card page-header-row" className="orders-card page-header-row"
sx={{ sx={{
mb: { xs: 2, sm: 2.5 }, mb: { xs: 1.5, sm: 2 },
display: 'flex', display: 'flex',
flexDirection: { xs: 'column', lg: 'row' }, flexDirection: { xs: 'column', lg: 'row' },
alignItems: { xs: 'stretch', lg: 'center' }, alignItems: { xs: 'stretch', lg: 'center' },
justifyContent: 'space-between', justifyContent: 'space-between',
gap: { xs: 2, lg: 3 }, gap: { xs: 1.25, lg: 2 },
px: { xs: 2, sm: 2.5 }, px: { xs: 1.75, sm: 2 },
py: { xs: 1.25, sm: 1.5 }, py: { xs: 0.75, sm: 0.9 },
flexShrink: 0 flexShrink: 0
}} }}
> >
@@ -1076,19 +1085,16 @@ const Createorder1 = () => {
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: 0.5, gap: 0,
minWidth: 0, minWidth: 0,
flex: { lg: '1 1 auto' } flex: { lg: '1 1 auto' }
}} }}
> >
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="h3" sx={{ fontWeight: 700, color: '#1e293b', lineHeight: 1.15 }}> <Typography sx={{ fontWeight: 700, color: '#1e293b', lineHeight: 1.15, fontSize: { xs: '17px', sm: '19px' } }}>
Create New Order Create New Order
</Typography> </Typography>
</Box> </Box>
<Typography variant="body1" sx={{ color: '#64748b', fontWeight: 500 }}>
Configure client coordinates, delivery payloads, and dispatch schedules in real-time.
</Typography>
</Box> </Box>
<Box <Box
@@ -1098,14 +1104,14 @@ const Createorder1 = () => {
flexDirection: { xs: 'column', sm: 'row' }, flexDirection: { xs: 'column', sm: 'row' },
alignItems: { xs: 'stretch', sm: 'center' }, alignItems: { xs: 'stretch', sm: 'center' },
justifyContent: { xs: 'stretch', lg: 'flex-end' }, justifyContent: { xs: 'stretch', lg: 'flex-end' },
gap: 1.5, gap: 1,
p: { xs: 0.5, sm: 1, lg: 1.5 }, p: 0,
width: { xs: '100%', lg: 'auto' }, width: { xs: '100%', lg: 'auto' },
flexShrink: 0 flexShrink: 0
}} }}
> >
{/* Choose App location */} {/* Choose App location */}
<Box sx={{ width: { xs: '100%', sm: 190, md: 210, xl: 230 } }}> <Box sx={{ width: { xs: '100%', sm: 180, md: 200, xl: 220 } }}>
<Autocomplete <Autocomplete
fullWidth fullWidth
autoFocus autoFocus
@@ -1113,6 +1119,7 @@ const Createorder1 = () => {
ref={locationRef} ref={locationRef}
options={locations || []} options={locations || []}
getOptionLabel={(option) => `${option.locationname}`} getOptionLabel={(option) => `${option.locationname}`}
className="header-compact-input"
onChange={(event, value, reason) => { onChange={(event, value, reason) => {
if (reason === 'clear') { if (reason === 'clear') {
setAppId(0); setAppId(0);
@@ -1142,12 +1149,12 @@ const Createorder1 = () => {
placeholder="Choose Location" placeholder="Choose Location"
label="Location" label="Location"
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', paddingLeft: '12px' } }} className="header-compact-tf"
InputProps={{ InputProps={{
...params.InputProps, ...params.InputProps,
startAdornment: ( startAdornment: (
<> <>
<FaLocationDot style={{ color: '#94a3b8', fontSize: 14, marginRight: 8, flexShrink: 0 }} /> <FaLocationDot style={{ color: '#94a3b8', fontSize: 12, marginRight: 6, flexShrink: 0 }} />
{params.InputProps.startAdornment} {params.InputProps.startAdornment}
</> </>
) )
@@ -1158,10 +1165,11 @@ const Createorder1 = () => {
</Box> </Box>
{/* Choose Client */} {/* Choose Client */}
<Box sx={{ width: { xs: '100%', sm: 190, md: 210, xl: 230 } }}> <Box sx={{ width: { xs: '100%', sm: 180, md: 200, xl: 220 } }}>
<Autocomplete <Autocomplete
fullWidth fullWidth
size="small" size="small"
className="header-compact-input"
options={tenantlist || []} options={tenantlist || []}
value={tenantValue} value={tenantValue}
onOpen={(event) => { onOpen={(event) => {
@@ -1200,12 +1208,12 @@ const Createorder1 = () => {
label="Client" label="Client"
inputRef={tenantRef} inputRef={tenantRef}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', paddingLeft: '12px' } }} className="header-compact-tf"
InputProps={{ InputProps={{
...params.InputProps, ...params.InputProps,
startAdornment: ( startAdornment: (
<> <>
<FaUser style={{ color: '#94a3b8', fontSize: 13, marginRight: 8, flexShrink: 0 }} /> <FaUser style={{ color: '#94a3b8', fontSize: 11, marginRight: 6, flexShrink: 0 }} />
{params.InputProps.startAdornment} {params.InputProps.startAdornment}
</> </>
) )
@@ -1216,7 +1224,7 @@ const Createorder1 = () => {
</Box> </Box>
{/* Business Location */} {/* Business Location */}
<Box sx={{ width: { xs: '100%', sm: 210, md: 230, xl: 250 } }}> <Box sx={{ width: { xs: '100%', sm: 200, md: 220, xl: 240 } }}>
{tenantLocations.length == 1 ? ( {tenantLocations.length == 1 ? (
<TextField <TextField
variant="outlined" variant="outlined"
@@ -1225,11 +1233,11 @@ const Createorder1 = () => {
label="Business Location" label="Business Location"
value={tenantLocations[0].locationname} value={tenantLocations[0].locationname}
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', paddingLeft: '12px' } }} className="header-compact-tf"
InputProps={{ InputProps={{
style: { color: theme.palette.primary.main }, style: { color: theme.palette.primary.main },
startAdornment: ( startAdornment: (
<MyLocationIcon style={{ color: '#94a3b8', fontSize: 16, marginRight: 8, flexShrink: 0 }} /> <MyLocationIcon style={{ color: '#94a3b8', fontSize: 14, marginRight: 6, flexShrink: 0 }} />
) )
}} }}
/> />
@@ -1237,6 +1245,7 @@ const Createorder1 = () => {
<Autocomplete <Autocomplete
fullWidth fullWidth
size="small" size="small"
className="header-compact-input"
value={locationValue} value={locationValue}
options={tenantLocations || []} options={tenantLocations || []}
getOptionLabel={(option) => `${option.locationname} (${option.suburb})` || ''} getOptionLabel={(option) => `${option.locationname} (${option.suburb})` || ''}
@@ -1275,12 +1284,12 @@ const Createorder1 = () => {
label="Business Location" label="Business Location"
color="primary" color="primary"
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', paddingLeft: '12px' } }} className="header-compact-tf"
InputProps={{ InputProps={{
...params.InputProps, ...params.InputProps,
startAdornment: ( startAdornment: (
<> <>
<MyLocationIcon style={{ color: '#94a3b8', fontSize: 16, marginRight: 8, flexShrink: 0 }} /> <MyLocationIcon style={{ color: '#94a3b8', fontSize: 14, marginRight: 6, flexShrink: 0 }} />
{params.InputProps.startAdornment} {params.InputProps.startAdornment}
</> </>
) )
@@ -1304,17 +1313,64 @@ const Createorder1 = () => {
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', 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 {/* Card 2: Route Planner (Pickup & Drop) — tighter padding on md+ since
the two panels now sit side-by-side and need the horizontal room. */} the two panels now sit side-by-side and need the horizontal room. */}
<Card className="orders-card" sx={{ p: { xs: 2, sm: 2.5, lg: 2 } }}> <Card className="orders-card" sx={{ p: { xs: 1.25, sm: 1.5, lg: 1.5 } }}>
{/* Two-step stepper: Pickup → Drop */}
<Box className="route-stepper">
<Box
className={`route-step ${routeStep === 1 ? 'is-active' : ''} ${pickupStepComplete ? 'is-done' : ''} step-pickup`}
onClick={() => setRouteStep(1)}
role="button"
tabIndex={0}
>
<Box className="route-step-index">
{pickupStepComplete && routeStep !== 1 ? <FaCheck /> : '1'}
</Box>
<Box className="route-step-text">
<Typography className="route-step-title">Pickup</Typography>
<Typography className="route-step-sub">Where to collect</Typography>
</Box>
</Box>
<Box className={`route-step-connector ${pickupStepComplete ? 'is-done' : ''}`}>
<Box className="route-step-line" />
<Box className="route-step-line-arrow">
<FaArrowRight />
</Box>
</Box>
<Box
className={`route-step ${routeStep === 2 ? 'is-active' : ''} step-drop ${!pickupStepComplete ? 'is-locked' : ''}`}
onClick={() => {
if (pickupStepComplete) {
setRouteStep(2);
} else {
opentoast('Please complete Pickup details first', 'warning', 2000);
}
}}
role="button"
tabIndex={0}
>
<Box className="route-step-index">2</Box>
<Box className="route-step-text">
<Typography className="route-step-title">Drop</Typography>
<Typography className="route-step-sub">Where to deliver</Typography>
</Box>
</Box>
</Box>
<Box className="route-flow"> <Box className="route-flow">
{/* Pickup Details Block */} {/* Pickup Details Block */}
<Box className="location-panel pickup-panel"> <Box
className="location-panel pickup-panel"
sx={{ display: routeStep === 1 ? 'block' : 'none' }}
>
<Box className="lp-header"> <Box className="lp-header">
<Box className="lp-header-title"> <Box className="lp-header-title">
<Box className="lp-badge"> <Box className="lp-badge">
@@ -1356,7 +1412,7 @@ const Createorder1 = () => {
<Typography className="field-group-caption"> <Typography className="field-group-caption">
Contact Contact
</Typography> </Typography>
<Grid container spacing={2.5}> <Grid container spacing={1.5} sx={{ mt: 0.5 }}>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <TextField
inputRef={textFieldRef1} inputRef={textFieldRef1}
@@ -1417,7 +1473,7 @@ const Createorder1 = () => {
</Grid> </Grid>
{/* Address Autocomplete */} {/* Address Autocomplete */}
<Typography className="field-group-caption" sx={{ mt: 3 }}> <Typography className="field-group-caption" sx={{ mt: 2 }}>
Address Lookup Address Lookup
</Typography> </Typography>
{addId1 == 0 ? ( {addId1 == 0 ? (
@@ -1508,10 +1564,10 @@ const Createorder1 = () => {
)} )}
{/* Address details */} {/* Address details */}
<Typography className="field-group-caption" sx={{ mt: 3 }}> <Typography className="field-group-caption" sx={{ mt: 2 }}>
Address Details Address Details
</Typography> </Typography>
<Grid container spacing={2.5}> <Grid container spacing={1.5} sx={{ mt: 0.5 }}>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <TextField
fullWidth fullWidth
@@ -1596,7 +1652,7 @@ const Createorder1 = () => {
{/* Save for later */} {/* Save for later */}
{showCheck1 == 1 && ( {showCheck1 == 1 && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
<Box className="save-later-pill"> <Box className="save-later-pill">
<FormControlLabel <FormControlLabel
control={ control={
@@ -1613,10 +1669,30 @@ const Createorder1 = () => {
</Box> </Box>
</Box> </Box>
)} )}
{/* Step navigation */}
<Box className="step-nav">
<Typography className="step-nav-hint">
{pickupStepComplete
? 'Pickup looks good. Proceed to Drop details.'
: 'Fill the required Pickup fields to continue.'}
</Typography>
<Button
className="step-nav-btn step-nav-next"
disabled={!pickupStepComplete}
endIcon={<FaArrowRight style={{ fontSize: 12 }} />}
onClick={() => setRouteStep(2)}
>
Continue to Drop
</Button>
</Box>
</Box> </Box>
{/* Drop Details Block */} {/* Drop Details Block */}
<Box className="location-panel drop-panel"> <Box
className="location-panel drop-panel"
sx={{ display: routeStep === 2 ? 'block' : 'none' }}
>
<Box className="lp-header"> <Box className="lp-header">
<Box className="lp-header-title"> <Box className="lp-header-title">
<Box className="lp-badge"> <Box className="lp-badge">
@@ -1656,7 +1732,7 @@ const Createorder1 = () => {
<Typography className="field-group-caption"> <Typography className="field-group-caption">
Contact Contact
</Typography> </Typography>
<Grid container spacing={2.5}> <Grid container spacing={1.5} sx={{ mt: 0.5 }}>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <TextField
inputRef={textFieldRef2} inputRef={textFieldRef2}
@@ -1715,7 +1791,7 @@ const Createorder1 = () => {
</Grid> </Grid>
{/* Address Autocomplete */} {/* Address Autocomplete */}
<Typography className="field-group-caption" sx={{ mt: 3 }}> <Typography className="field-group-caption" sx={{ mt: 2 }}>
Address Lookup Address Lookup
</Typography> </Typography>
{addId2 == 0 ? ( {addId2 == 0 ? (
@@ -1806,10 +1882,10 @@ const Createorder1 = () => {
)} )}
{/* Address details */} {/* Address details */}
<Typography className="field-group-caption" sx={{ mt: 3 }}> <Typography className="field-group-caption" sx={{ mt: 2 }}>
Address Details Address Details
</Typography> </Typography>
<Grid container spacing={2.5}> <Grid container spacing={1.5} sx={{ mt: 0.5 }}>
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<TextField <TextField
fullWidth fullWidth
@@ -1894,7 +1970,7 @@ const Createorder1 = () => {
{/* Save for later */} {/* Save for later */}
{showCheck2 == 1 && ( {showCheck2 == 1 && (
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
<Box className="save-later-pill"> <Box className="save-later-pill">
<FormControlLabel <FormControlLabel
control={ control={
@@ -1911,23 +1987,37 @@ const Createorder1 = () => {
</Box> </Box>
</Box> </Box>
)} )}
{/* Step navigation */}
<Box className="step-nav">
<Button
className="step-nav-btn step-nav-back"
startIcon={<FaArrowLeft style={{ fontSize: 12 }} />}
onClick={() => setRouteStep(1)}
>
Back to Pickup
</Button>
<Typography className="step-nav-hint">
Review the route below once Drop is filled.
</Typography>
</Box>
</Box> </Box>
</Box> </Box>
</Card> </Card>
{/* Card 3: Cargo & Dispatch Logistics */} {/* Card 3: Cargo & Dispatch Logistics */}
<Card className="orders-card" sx={{ p: { xs: 2, sm: 2.5, lg: 3 } }}> <Card className="orders-card" sx={{ p: { xs: 1.5, sm: 1.75, lg: 2 } }}>
<Box className="section-title-bar" sx={{ mb: 2.5 }}> <Box className="section-title-bar" sx={{ mb: 1.25 }}>
<Typography variant="h5" sx={{ fontWeight: 600, color: '#1e293b' }}> <Typography sx={{ fontWeight: 700, color: '#1e293b', fontSize: '15px', letterSpacing: '-0.01em' }}>
Cargo & Dispatch Logistics Cargo &amp; Dispatch Logistics
</Typography> </Typography>
</Box> </Box>
<Grid container spacing={3} alignItems="stretch"> <Grid container spacing={1.5} alignItems="stretch">
{/* Section Header: Cargo Details */} {/* Section Header: Cargo Details */}
<Grid item xs={12}> <Grid item xs={12} sx={{ pt: '0 !important' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 0.5 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.25 }}>
<Typography sx={{ fontWeight: 700, fontSize: '11px', color: '#64748b', letterSpacing: '0.8px', textTransform: 'uppercase' }}> <Typography sx={{ fontWeight: 700, fontSize: '10.5px', color: '#64748b', letterSpacing: '0.7px', textTransform: 'uppercase' }}>
Cargo Details Cargo Details
</Typography> </Typography>
<Box sx={{ flex: 1, height: '1px', background: 'linear-gradient(90deg, #eef2f6 0%, transparent 100%)' }} /> <Box sx={{ flex: 1, height: '1px', background: 'linear-gradient(90deg, #eef2f6 0%, transparent 100%)' }} />
@@ -1946,7 +2036,7 @@ const Createorder1 = () => {
sx={{ sx={{
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
borderRadius: '12px', borderRadius: '12px',
height: '42px', height: '38px',
paddingTop: '0px !important', paddingTop: '0px !important',
paddingBottom: '0px !important' paddingBottom: '0px !important'
} }
@@ -1957,7 +2047,7 @@ const Createorder1 = () => {
label="Category" label="Category"
size="small" size="small"
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '42px' } }} sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '38px' } }}
InputProps={{ InputProps={{
...params.InputProps, ...params.InputProps,
startAdornment: ( startAdornment: (
@@ -1996,7 +2086,7 @@ const Createorder1 = () => {
setCollectionamt(e.target.value); setCollectionamt(e.target.value);
}} }}
inputProps={{ min: 0 }} inputProps={{ min: 0 }}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '42px' } }} sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '38px' } }}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
@@ -2021,7 +2111,7 @@ const Createorder1 = () => {
setQuantity(e.target.value); setQuantity(e.target.value);
}} }}
inputProps={{ min: 1 }} inputProps={{ min: 1 }}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '42px' } }} sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '38px' } }}
InputProps={{ InputProps={{
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
@@ -2033,73 +2123,20 @@ const Createorder1 = () => {
</Stack> </Stack>
</Grid> </Grid>
{/* Row 2: Weight Range Selector */}
<Grid item xs={12}>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 1 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#475569', fontSize: 13.5 }}>
Select Cargo Weight Range <span style={{ color: '#ef4444' }}>*</span>
</Typography>
{weight && (
<Typography variant="body2" sx={{
fontWeight: 600,
color: weight === '1-10kgs' ? '#0ea5e9' : weight === '11-20kgs' ? '#a855f7' : '#6366f1',
fontSize: 12.5,
textTransform: 'capitalize'
}}>
{weight === '1-10kgs' ? 'Light' : weight === '11-20kgs' ? 'Medium' : 'Heavy'} selected
</Typography>
)}
</Stack>
<div className="weight-selector-grid">
<div
className={`weight-card-btn weight-light ${weight === '1-10kgs' ? 'active' : ''}`}
onClick={() => {
handleChipClick('1-10kgs');
setWeight('1-10kgs');
}}
>
<FaBox className="weight-card-icon" />
<div className="weight-card-label">Light Cargo (1-10 kgs)</div>
<div className="weight-card-desc">Parcels, retail envelopes</div>
</div>
<div
className={`weight-card-btn weight-medium ${weight === '11-20kgs' ? 'active' : ''}`}
onClick={() => {
handleChipClick('11-20kgs');
setWeight('11-20kgs');
}}
>
<FaBoxes className="weight-card-icon" />
<div className="weight-card-label">Medium Cargo (11-20 kgs)</div>
<div className="weight-card-desc">Grocery crates, retail goods</div>
</div>
<div
className={`weight-card-btn weight-heavy ${weight === '21-30kgs' ? 'active' : ''}`}
onClick={() => {
handleChipClick('21-30kgs');
setWeight('21-30kgs');
}}
>
<FaTruck className="weight-card-icon" />
<div className="weight-card-label">Heavy Cargo (21-30 kgs)</div>
<div className="weight-card-desc">Industrial parts, heavy shipments</div>
</div>
</div>
</Grid>
{/* Section Header: Handover & Schedule */} {/* Section Header: Handover & Schedule */}
<Grid item xs={12} sx={{ mt: 0.5, pb: 0 }}> <Grid item xs={12} sx={{ mt: 0.5, pb: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography sx={{ display: 'flex', alignItems: 'center', gap: 1, fontWeight: 700, fontSize: '11px', color: '#64748b', letterSpacing: '0.8px', textTransform: 'uppercase' }}> <Typography sx={{ display: 'flex', alignItems: 'center', gap: 0.75, fontWeight: 700, fontSize: '10.5px', color: '#64748b', letterSpacing: '0.7px', textTransform: 'uppercase' }}>
<CalendarOutlined style={{ fontSize: '12px', color: '#65387a' }} /> <CalendarOutlined style={{ fontSize: '11px', color: '#65387a' }} />
Schedule Details Schedule Details
</Typography> </Typography>
<Box sx={{ flex: 1, height: '1px', background: 'linear-gradient(90deg, #eef2f6 0%, transparent 100%)' }} /> <Box sx={{ flex: 1, height: '1px', background: 'linear-gradient(90deg, #eef2f6 0%, transparent 100%)' }} />
</Box> </Box>
</Grid> </Grid>
{/* Nested Grid Container with tighter spacing={2} to eliminate excessive gaps */} {/* Nested Grid Container with tight spacing to eliminate excessive gaps */}
<Grid item xs={12} sx={{ pt: '0px !important' }}> <Grid item xs={12} sx={{ pt: '0px !important' }}>
<Grid container spacing={2}> <Grid container spacing={1.5} sx={{ mt: 0.5 }}>
{/* Row 3: Pickup Date & Time Slot (Side-by-Side) */} {/* Row 3: Pickup Date & Time Slot (Side-by-Side) */}
<Grid item xs={12} sm={6}> <Grid item xs={12} sm={6}>
<LocalizationProvider dateAdapter={AdapterDayjs}> <LocalizationProvider dateAdapter={AdapterDayjs}>
@@ -2130,7 +2167,7 @@ const Createorder1 = () => {
width: '100%', width: '100%',
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
borderRadius: '12px', borderRadius: '12px',
height: '42px' height: '38px'
} }
}} }}
slotProps={{ slotProps={{
@@ -2148,7 +2185,7 @@ const Createorder1 = () => {
sx: { sx: {
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
borderRadius: '12px', borderRadius: '12px',
height: '42px', height: '38px',
paddingLeft: '10px' paddingLeft: '10px'
} }
} }
@@ -2168,7 +2205,7 @@ const Createorder1 = () => {
width: '100%', width: '100%',
'& .MuiOutlinedInput-root': { '& .MuiOutlinedInput-root': {
borderRadius: '12px', borderRadius: '12px',
height: '42px', height: '38px',
paddingTop: '0px !important', paddingTop: '0px !important',
paddingBottom: '0px !important' paddingBottom: '0px !important'
} }
@@ -2194,7 +2231,7 @@ const Createorder1 = () => {
placeholder="Select Pickup Slot" placeholder="Select Pickup Slot"
fullWidth fullWidth
InputLabelProps={{ shrink: true }} InputLabelProps={{ shrink: true }}
sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '42px' } }} sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px', height: '38px' } }}
InputProps={{ InputProps={{
...params.InputProps, ...params.InputProps,
startAdornment: ( startAdornment: (
@@ -2209,95 +2246,6 @@ const Createorder1 = () => {
/> />
</Grid> </Grid>
{/* Row 4: Special Dispatch Notes & SMS Updates (Side-by-Side) */}
<Grid item xs={12} sm={6}>
<TextField
id="outlined-multiline-static"
label="Special Dispatch Notes"
size="small"
fullWidth
InputLabelProps={{ shrink: true }}
sx={{
width: '100%',
'& .MuiOutlinedInput-root': { borderRadius: '12px', height: '42px' }
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<FileTextOutlined style={{ color: '#94a3b8', fontSize: '13px' }} />
</InputAdornment>
)
}}
placeholder="Provide gate codes, call instructions, or special cargo care instructions..."
value={otherinstructions}
onChange={(e) => setOtherinstructions(e.target.value)}
/>
</Grid>
<Grid item xs={12} sm={6}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
spacing={1.5}
onClick={() => 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)'
}
}}
>
<Stack direction="row" spacing={1} alignItems="center" sx={{ minWidth: 0 }}>
<Box
sx={{
width: 26,
height: 26,
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: isSms === 1
? 'linear-gradient(135deg, #1890ff, #65387a)'
: '#eef2f6',
color: isSms === 1 ? '#ffffff' : '#94a3b8',
transition: 'all 0.2s ease',
flexShrink: 0
}}
>
<MessageOutlined style={{ fontSize: 11.5 }} />
</Box>
<Box sx={{ minWidth: 0 }}>
<Typography variant="h6" sx={{ fontWeight: 600, color: '#1e293b', fontSize: 12.5, lineHeight: 1.1 }}>
SMS Updates
</Typography>
<Typography variant="body2" sx={{ color: '#64748b', fontSize: 10, mt: 0.1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title="Send tracking updates automatically">
Track automatically
</Typography>
</Box>
</Stack>
<Switch
size="small"
checked={isSms === 1}
onChange={(e) => {
e.stopPropagation();
setIsSms(e.target.checked ? 1 : 0);
}}
/>
</Stack>
</Grid>
</Grid> </Grid>
</Grid> </Grid>
</Grid> </Grid>
@@ -2315,12 +2263,12 @@ const Createorder1 = () => {
height: 'fit-content' height: 'fit-content'
}} }}
> >
<Stack spacing={4}> <Stack spacing={2}>
{/* Map Card */} {/* Map Card */}
<Card className="orders-card" sx={{ p: 2, display: 'flex', flexDirection: 'column' }}> <Card className="orders-card" sx={{ p: 1.5, display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1, color: '#1e293b' }}> <Typography sx={{ fontWeight: 700, mb: 1.25, display: 'flex', alignItems: 'center', gap: 0.75, color: '#1e293b', fontSize: '14px', letterSpacing: '-0.01em' }}>
<MyLocationIcon sx={{ color: '#1890ff', fontSize: 20 }} /> <MyLocationIcon sx={{ color: '#1890ff', fontSize: 16 }} />
Live Route Preview Live Route Preview
</Typography> </Typography>
<div className="map-preview-wrapper"> <div className="map-preview-wrapper">
@@ -2328,24 +2276,99 @@ const Createorder1 = () => {
</div> </div>
</Card> </Card>
{/* Delivery Preferences — Dispatch Notes & SMS Updates */}
<Card className="orders-card delivery-prefs-card" sx={{ p: 1.5 }}>
<Box className="delivery-prefs-header">
<Typography className="delivery-prefs-title">Delivery Preferences</Typography>
<Typography className="delivery-prefs-sub">Customer notifications &amp; dispatch instructions</Typography>
</Box>
<Box className="delivery-prefs-row">
<Box className="delivery-prefs-field">
<label className="delivery-prefs-label" htmlFor="dispatch-notes-input">
<FileTextOutlined style={{ fontSize: 11, color: '#65387a' }} />
Special Dispatch Notes
</label>
<TextField
id="dispatch-notes-input"
size="small"
fullWidth
placeholder="Gate codes, call instructions, special cargo care…"
value={otherinstructions}
onChange={(e) => 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'
}
}}
/>
</Box>
<Box
className={`sms-toggle-tile ${isSms === 1 ? 'is-active' : ''}`}
onClick={() => setIsSms(isSms === 1 ? 0 : 1)}
role="button"
tabIndex={0}
>
<Box className="sms-toggle-left">
<Box className="sms-toggle-icon">
<MessageOutlined style={{ fontSize: 13 }} />
</Box>
<Box sx={{ minWidth: 0 }}>
<Typography className="sms-toggle-title">SMS Updates</Typography>
<Typography className="sms-toggle-sub">Auto-notify customer on dispatch &amp; delivery</Typography>
</Box>
</Box>
<Switch
size="small"
checked={isSms === 1}
onChange={(e) => {
e.stopPropagation();
setIsSms(e.target.checked ? 1 : 0);
}}
/>
</Box>
</Box>
</Card>
{/* Pricing breakdown card */} {/* Pricing breakdown card */}
<Card className="orders-card pricing-summary-card"> <Card className="orders-card pricing-summary-card">
<Typography variant="h5" sx={{ fontWeight: 600, mb: 2.5, color: '#1e293b' }}> <Box className="pricing-header">
Pricing & Dispatch Metrics <Typography className="pricing-title">Pricing &amp; Dispatch</Typography>
</Typography> <Typography className="pricing-subtitle">Live cost estimate</Typography>
</Box>
<div className="price-metric-item"> <div className="price-metric-item">
<div className="price-metric-label"> <div className="price-metric-label">
<span style={{ fontSize: '20px' }}>📍</span> Delivery Distance <span className="price-metric-icon icon-distance">
<FaRoute />
</span>
<span>Delivery Distance</span>
</div> </div>
<div className={`price-metric-value ${showDistance ? 'highlight' : ''}`}> <div className={`price-metric-value ${showDistance ? 'highlight' : ''}`}>
{showDistance ? `${distance} km` : '--'} {showDistance ? `${distance} km` : ''}
</div> </div>
</div> </div>
<div className="price-metric-item"> <div className="price-metric-item">
<div className="price-metric-label"> <div className="price-metric-label">
<span style={{ fontSize: '20px' }}>💵</span> Base Fare ({minKm} km limit) <span className="price-metric-icon icon-base">
<FaMoneyBillWave />
</span>
<span>
Base Fare
<span className="price-metric-sub"> · {minKm} km</span>
</span>
</div> </div>
<div className="price-metric-value"> <div className="price-metric-value">
{basePrice ? `${basePrice.toFixed(2)}` : '₹0.00'} {basePrice ? `${basePrice.toFixed(2)}` : '₹0.00'}
@@ -2354,28 +2377,36 @@ const Createorder1 = () => {
<div className="price-metric-item"> <div className="price-metric-item">
<div className="price-metric-label"> <div className="price-metric-label">
<span style={{ fontSize: '20px' }}>📈</span> Rate per km <span className="price-metric-icon icon-rate">
<FaChartLine />
</span>
<span>Rate per km</span>
</div> </div>
<div className="price-metric-value"> <div className="price-metric-value">
{pricePerKm ? `${pricePerKm.toFixed(2)}/km` : '₹0.00/km'} {pricePerKm ? `${pricePerKm.toFixed(2)}` : '₹0.00'}
<span className="price-metric-unit">/km</span>
</div> </div>
</div> </div>
{/* Total Cost Display */} {/* Total Cost Display */}
{showDistance && ( {showDistance && (
<div className="total-charge-badge"> <div className="total-charge-badge">
<div className="total-charge-left">
<FaReceipt className="total-charge-icon" />
<div className="total-charge-label">Total Delivery Charge</div> <div className="total-charge-label">Total Delivery Charge</div>
</div>
<div className="total-charge-val">{totalCharge.toFixed(2)}</div> <div className="total-charge-val">{totalCharge.toFixed(2)}</div>
</div> </div>
)} )}
{/* Submit button */} {/* Submit button */}
<Box sx={{ mt: 3 }}> <Box sx={{ mt: 1.5 }}>
<AnimateButton> <AnimateButton>
<Button <Button
fullWidth fullWidth
className="gradient-btn-create" className="gradient-btn-create"
disabled={!showDistance || !selectedtime || !pickupSlot} disabled={!showDistance || !selectedtime || !pickupSlot}
startIcon={!btnLoading && <FaPaperPlane style={{ fontSize: 11 }} />}
onClick={() => { onClick={() => {
setLoading(true); setLoading(true);
setBtnLoading(true); setBtnLoading(true);
@@ -2387,7 +2418,7 @@ const Createorder1 = () => {
}} }}
> >
{btnLoading ? ( {btnLoading ? (
<CircularProgress color="inherit" size={24} thickness={4} /> <CircularProgress color="inherit" size={16} thickness={5} />
) : ( ) : (
'Dispatch Delivery Order' 'Dispatch Delivery Order'
)} )}