updated the ui for the mobile view

This commit is contained in:
2026-06-09 13:12:18 +05:30
parent 8513c7c2ec
commit f18b680247
29 changed files with 3664 additions and 225 deletions

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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>