new chnages in ui
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 (8–9 AM, 12 PM–4 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,68 +2185,62 @@ 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) => {
|
||||||
<div key={r.id} className="rcard" onClick={() => handleRiderFocus(r)} style={{ animationDelay: `${i * 0.05}s` }}>
|
const total = r.orders.length;
|
||||||
<div className="rcard-top">
|
const delivered = r.orders.filter((o) =>
|
||||||
<div className="rcard-emo" style={{ background: `${r.color}18`, borderColor: `${r.color}50`, color: r.color }}><MdTwoWheeler /></div>
|
FINAL_STATUSES.has(String(o.orderstatus || '').toLowerCase())
|
||||||
<div className="rcard-info">
|
).length;
|
||||||
<div className="rcard-name">{r.riderName}</div>
|
const isDone = total > 0 && delivered >= total;
|
||||||
<div className="rcard-zone">{r.orders[0]?.zone_name || locationName || 'Local'} · {new Set(r.orders.map(o => o.trip_number || 1)).size} trips</div>
|
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>
|
||||||
<div className="rcard-badge" style={{ background: `${r.color}18`, color: r.color }}>{r.orders.length}</div>
|
|
||||||
</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>)}
|
// Returns true when the order's centered popup should stay open even after
|
||||||
</div>
|
// the cursor leaves the marker: either explicitly pinned via click, or the
|
||||||
</div>
|
// 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
|
// Shared order-popup body used by both the planned-route number markers
|
||||||
// and the Compare mode actual-track drop pins. Extracted so clicking a
|
// and the Compare mode actual-track drop pins. Extracted so clicking a
|
||||||
// pin on either layer surfaces the exact same Timeline + Details + KM
|
// pin on either layer surfaces the exact same Timeline + Details + KM
|
||||||
// chips — operators learn the layout once and trust it everywhere.
|
// 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 renderOrderPopupContent = (o) => {
|
||||||
const statusStyle = getStatusStyle(o.orderstatus);
|
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 (
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -1299,4 +1415,464 @@
|
|||||||
main:has(.orders-workspace-bg) {
|
main:has(.orders-workspace-bg) {
|
||||||
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%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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 & 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 & 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 */}
|
{/* 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 & 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-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 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'
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user