updated the ui for the mobile view
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTheme, useMediaQuery } from '@mui/material';
|
||||
import {
|
||||
MdPublic,
|
||||
MdSwapHoriz,
|
||||
@@ -60,6 +61,13 @@ function CompareDataPanel({
|
||||
setExpandedSeqGroups,
|
||||
onClose
|
||||
}) {
|
||||
// Mobile flag — gates only layout (a className on the root <aside> that a
|
||||
// scoped media-query block in Dispatch.css uses to stack the side-by-side
|
||||
// timing/clock columns vertically and let stat chips wrap). No data,
|
||||
// handler, or derivation behaviour changes on any breakpoint.
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
// All derivations live in a single useMemo so the cost of re-running
|
||||
// them is paid only when an upstream input actually changes — not on
|
||||
// every parent render (e.g. cursor moving over the map, sync toggle
|
||||
@@ -294,7 +302,10 @@ function CompareDataPanel({
|
||||
} = view;
|
||||
|
||||
return (
|
||||
<aside id="compare-data-panel" className="compare-data-panel">
|
||||
<aside
|
||||
id="compare-data-panel"
|
||||
className={`compare-data-panel${isMobile ? ' cdp-is-mobile' : ''}`}
|
||||
>
|
||||
<div className="cdp-head">
|
||||
<div className="cdp-head-title">
|
||||
<span
|
||||
|
||||
@@ -9244,6 +9244,39 @@
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Compare data panel — mobile layout (gated by the cdp-is-mobile
|
||||
class added in CompareDataPanel.js when viewport < md). Desktop
|
||||
(>= md) renders without this class, so nothing here applies.
|
||||
Only stacking / wrapping — no behaviour, colours, or content
|
||||
change. The panel occupies the full-width compare column on
|
||||
phones; the side-by-side clock and stat grids stack vertically
|
||||
and the chip rows are allowed to wrap so nothing overflows.
|
||||
============================================================ */
|
||||
.dispatch-container .compare-data-panel.cdp-is-mobile {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* 3-column clock (First | duration | Last) → single column stack. */
|
||||
.dispatch-container .compare-data-panel.cdp-is-mobile .cdp-timing-clock {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
/* The center track is horizontal between two side columns on desktop;
|
||||
give it a sane height when it becomes a full-width stacked row. */
|
||||
.dispatch-container .compare-data-panel.cdp-is-mobile .cdp-clock-track {
|
||||
min-width: 0;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
/* Timing stats (avg/stop + avg speed) → stack instead of side by side.
|
||||
(The chip rows — highlight-meta / dev-meta / step-deltas / trip-stats —
|
||||
already carry flex-wrap on desktop, so no extra wrap rules are needed.) */
|
||||
.dispatch-container .compare-data-panel.cdp-is-mobile .cdp-timing-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Sidebar Rider Card Est. Meters badge */
|
||||
.dispatch-container .rcard-est-meters {
|
||||
display: inline-flex;
|
||||
@@ -10691,4 +10724,82 @@
|
||||
color: #b45309;
|
||||
font-weight: 600;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
Mobile / app-like layout (≤ 768px). Purely a LAYOUT pass — no behaviour
|
||||
changes. On phones the side-by-side map + sidebar collapses into a single
|
||||
vertical stack: the live map takes the top of the screen full-width, and
|
||||
the rider/batch sidebar flows below it full-width and scrolls. Desktop
|
||||
(≥ md) is untouched; every rule here is gated inside this media query.
|
||||
========================================================================= */
|
||||
@media (max-width: 768px) {
|
||||
/* The standalone page un-does MainCard's 24px padding via negative margin
|
||||
+ 100vw-ish sizing; keep that, just allow the inner body to grow with
|
||||
its stacked children instead of being clipped to a single row height. */
|
||||
.dispatch-container {
|
||||
height: auto;
|
||||
min-height: calc(100vh - 88px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Stack map (first) over sidebar (second). #body is a flex row on desktop;
|
||||
flip it to a column so the children lay out vertically. */
|
||||
.dispatch-container #body {
|
||||
flex-direction: column;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Map: full-width, fixed viewport-relative height so Leaflet has an
|
||||
explicit box to size its canvas against. Order it first. */
|
||||
.dispatch-container #map-wrap,
|
||||
.dispatch-container #map-wrap.compare-split {
|
||||
order: 1;
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 48vh;
|
||||
min-height: 320px;
|
||||
margin-right: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* Sidebar: full-width below the map, natural height, internal scroll.
|
||||
Force it visible even if the collapse state is set (the horizontal
|
||||
collapse-to-width:0 animation is meaningless when stacked). */
|
||||
.dispatch-container #sidebar,
|
||||
.dispatch-container #body.sidebar-collapsed #sidebar {
|
||||
order: 2;
|
||||
width: 100%;
|
||||
flex: 1 1 auto;
|
||||
flex-basis: auto;
|
||||
min-width: 0;
|
||||
max-height: none;
|
||||
border-right: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
border-right-color: var(--border);
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* The collapse peek-tab is anchored to a left:Npx column edge that no
|
||||
longer exists once stacked — hide it so it doesn't float over the map. */
|
||||
.dispatch-container .sidebar-toggle-tab {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Header — let it wrap cleanly instead of overflowing a fixed-height row. */
|
||||
.dispatch-container #hdr {
|
||||
height: auto;
|
||||
min-height: 42px;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* The BI / analysis view is its own scroll surface — let it grow. */
|
||||
.dispatch-container #dispatch-analysis {
|
||||
overflow-y: visible;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
Tabs,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography
|
||||
Typography,
|
||||
useMediaQuery,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -278,6 +280,8 @@ const Preview = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const stateData = location.state || {};
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
// SINGLE SOURCE OF TRUTH: every Change Rider / Reconcile / Re-Assign goes
|
||||
// through this state. The Dispatch tab renders from it, the Reconcile tab
|
||||
@@ -553,7 +557,12 @@ const Preview = () => {
|
||||
</Backdrop>
|
||||
|
||||
<Box sx={{ py: 1.25, px: 2, borderBottom: '1px solid #eef2f6' }}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
||||
<Stack
|
||||
direction={{ xs: 'column', md: 'row' }}
|
||||
alignItems={{ xs: 'stretch', md: 'center' }}
|
||||
justifyContent="space-between"
|
||||
spacing={1.25}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Tooltip title="Back to orders" placement="top">
|
||||
<IconButton
|
||||
@@ -567,11 +576,16 @@ const Preview = () => {
|
||||
Assign Orders
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Stack
|
||||
direction={{ xs: 'column', sm: 'row' }}
|
||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||
spacing={1}
|
||||
sx={{ width: { xs: '100%', md: 'auto' } }}
|
||||
>
|
||||
<Autocomplete
|
||||
options={tuningTypes || []}
|
||||
getOptionLabel={(option) => option.type}
|
||||
sx={{ minWidth: 250, maxWidth: 600, flex: 1 }}
|
||||
sx={{ minWidth: { xs: 0, sm: 250 }, maxWidth: 600, flex: 1, width: { xs: '100%', sm: 'auto' } }}
|
||||
renderInput={(params) => <TextField {...params} label="Hyper Tuning" />}
|
||||
onChange={(e, val, reason) => {
|
||||
if (reason === 'clear') handleCreateDelivery(null);
|
||||
@@ -582,6 +596,7 @@ const Preview = () => {
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<IoReload />}
|
||||
fullWidth={isMobile}
|
||||
onClick={() => {
|
||||
setIsLoading(true);
|
||||
handleCreateDelivery('reshuffle');
|
||||
@@ -600,7 +615,12 @@ const Preview = () => {
|
||||
</Box>
|
||||
|
||||
<Box sx={{ px: 2, borderBottom: '1px solid #eef2f6' }}>
|
||||
<Tabs value={tabValue} onChange={(e, v) => setTabValue(v)} sx={{ minHeight: 40 }}>
|
||||
<Tabs
|
||||
value={tabValue}
|
||||
onChange={(e, v) => setTabValue(v)}
|
||||
variant={isMobile ? 'fullWidth' : 'standard'}
|
||||
sx={{ minHeight: 40 }}
|
||||
>
|
||||
<Tab label="Dispatch" sx={{ minHeight: 40, textTransform: 'none', fontWeight: 600 }} />
|
||||
<Tab label="Reconcile" sx={{ minHeight: 40, textTransform: 'none', fontWeight: 600 }} />
|
||||
</Tabs>
|
||||
@@ -647,8 +667,13 @@ const Preview = () => {
|
||||
{reconcileRiders.map((r) => {
|
||||
const totalKms = r.orders.reduce((s, o) => s + parseFloat(o.actualkms || o.kms || 0), 0);
|
||||
return (
|
||||
<Card key={r.rider_id} sx={{ p: 2, borderRadius: '12px', boxShadow: '0 1px 3px rgba(15,23,42,0.06)' }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1.25 }}>
|
||||
<Card key={r.rider_id} sx={{ p: { xs: 1.5, md: 2 }, borderRadius: '12px', boxShadow: '0 1px 3px rgba(15,23,42,0.06)' }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{ mb: 1.25, flexWrap: 'wrap', gap: 1 }}
|
||||
>
|
||||
<Stack direction="row" alignItems="center" gap={1.25}>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -732,7 +757,8 @@ const Preview = () => {
|
||||
startIcon={<MdSwapHoriz />}
|
||||
onClick={handleReconcile}
|
||||
disabled={reconcileLoading || dirtyRiderIds.size === 0}
|
||||
sx={{ minWidth: 220, borderRadius: '10px', textTransform: 'none', fontWeight: 700 }}
|
||||
fullWidth={isMobile}
|
||||
sx={{ minWidth: { xs: 0, sm: 220 }, borderRadius: '10px', textTransform: 'none', fontWeight: 700 }}
|
||||
>
|
||||
{reconcileLoading
|
||||
? 'Reconciling...'
|
||||
@@ -748,22 +774,35 @@ const Preview = () => {
|
||||
</Box>
|
||||
|
||||
<Box sx={{ px: 2, py: 1.25, borderTop: '1px solid #eef2f6' }}>
|
||||
<Stack direction="row" gap={2} alignItems="center" justifyContent="end">
|
||||
<Stack
|
||||
direction={{ xs: 'column-reverse', sm: 'row' }}
|
||||
gap={{ xs: 1, sm: 2 }}
|
||||
alignItems={{ xs: 'stretch', sm: 'center' }}
|
||||
justifyContent="end"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
startIcon={<ArrowBackIcon />}
|
||||
fullWidth={isMobile}
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button variant="contained" onClick={handleFinalCreateDelivery}>
|
||||
<Button variant="contained" fullWidth={isMobile} onClick={handleFinalCreateDelivery}>
|
||||
Assign Orders
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
<Dialog open={changeDialogOpen} onClose={() => setChangeDialogOpen(false)} maxWidth="xs" fullWidth>
|
||||
<Dialog
|
||||
open={changeDialogOpen}
|
||||
onClose={() => setChangeDialogOpen(false)}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
fullScreen={isMobile}
|
||||
PaperProps={{ sx: { borderRadius: { xs: 0, sm: 3 } } }}
|
||||
>
|
||||
<DialogTitle sx={{ fontWeight: 700 }}>Change Rider</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography sx={{ mb: 2, fontSize: 13, color: 'text.secondary' }}>
|
||||
@@ -779,9 +818,11 @@ const Preview = () => {
|
||||
renderInput={(params) => <TextField {...params} label="New rider" placeholder="Pick a rider" />}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setChangeDialogOpen(false)}>Cancel</Button>
|
||||
<Button variant="contained" disabled={!selectedNewRider} onClick={confirmChangeRider}>
|
||||
<DialogActions sx={{ px: 3, pb: 2, flexDirection: { xs: 'column-reverse', sm: 'row' }, gap: { xs: 1, sm: 0 } }}>
|
||||
<Button fullWidth={isMobile} onClick={() => setChangeDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" fullWidth={isMobile} disabled={!selectedNewRider} onClick={confirmChangeRider}>
|
||||
Change Rider
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
Reference in New Issue
Block a user