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