Files
doormile_hub_console/src/components/DateRangePicker.jsx
Thiru-tenext ef0b14d254 feat: Implement date range picker and enhance order assignment functionality
- Added DateRangePicker component for selecting date ranges in OrderAssignment.
- Updated OrderAssignment to fetch bookings based on selected date range.
- Enhanced status handling in OrderAssignment with new status chip display logic.
- Refactored KPI card implementation in RiderRoutes and Riders to use shared StatCard component.
- Improved ProfileDrawer to retain rider data during close transition.
- Fixed minor text formatting in Routing component.
2026-07-09 16:28:45 +05:30

215 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { Box, Stack, Typography, IconButton, Button, Popover, Divider, Chip, useMediaQuery } from '@mui/material';
import { alpha } from '@mui/material/styles';
import dayjs from 'dayjs';
import CalendarTodayOutlinedIcon from '@mui/icons-material/CalendarTodayOutlined';
import ChevronLeftRoundedIcon from '@mui/icons-material/ChevronLeftRounded';
import ChevronRightRoundedIcon from '@mui/icons-material/ChevronRightRounded';
import ArrowRightAltRoundedIcon from '@mui/icons-material/ArrowRightAltRounded';
// ─────────────────────────────────────────────────────────────────────────────
// Shared date-range picker — the same two-month brand calendar used on the
// Dashboard, packaged for reuse. Controlled: pass `value={{ from, to }}` (both
// 'YYYY-MM-DD') and an `onChange({ from, to })` handler. Optionally cap the
// selectable range with `maxDate` (defaults to today, so no future dates).
// ─────────────────────────────────────────────────────────────────────────────
export const DATE_FMT = 'YYYY-MM-DD';
const BRAND = '#C01227';
const DAY_LABELS = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
// A single compact month grid.
function MonthGrid({ view, start, end, max, onDay }) {
const gridStart = view.startOf('month').subtract(view.startOf('month').day(), 'day');
const cells = Array.from({ length: 42 }, (_, i) => gridStart.add(i, 'day'));
return (
<Box sx={{ width: 224 }}>
<Typography align="center" sx={{ fontWeight: 700, fontSize: '0.82rem', color: '#212529', mb: 0.75 }}>
{view.format('MMMM YYYY')}
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', mb: 0.25 }}>
{DAY_LABELS.map((d, i) => (
<Typography key={i} align="center" sx={{ fontSize: '0.62rem', fontWeight: 600, color: '#B0B5BA', py: 0.25 }}>
{d}
</Typography>
))}
</Box>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)' }}>
{cells.map((d) => {
const inMonth = d.month() === view.month();
const isStart = start && d.isSame(start, 'day');
const isEnd = end && d.isSame(end, 'day');
const isEndpoint = isStart || isEnd;
const inRange = start && end && d.isAfter(start, 'day') && d.isBefore(end, 'day');
const isToday = d.isSame(dayjs(), 'day');
const disabled = max && d.isAfter(max, 'day');
return (
<Box key={d.format(DATE_FMT)} sx={{ display: 'flex', justifyContent: 'center', bgcolor: inRange ? alpha(BRAND, 0.07) : 'transparent',
borderTopLeftRadius: isStart ? 6 : 0, borderBottomLeftRadius: isStart ? 6 : 0,
borderTopRightRadius: isEnd ? 6 : 0, borderBottomRightRadius: isEnd ? 6 : 0 }}>
<Box
component="button"
type="button"
disabled={disabled}
onClick={() => onDay(d)}
sx={{
width: 28, height: 28, m: '1px', border: 'none', cursor: disabled ? 'default' : 'pointer',
borderRadius: 1.5, fontSize: '0.72rem', fontFamily: 'inherit',
fontWeight: isEndpoint ? 700 : 500,
color: disabled ? '#D5D9DD' : isEndpoint ? '#fff' : inMonth ? '#3C4043' : '#C4C9CE',
bgcolor: isEndpoint ? BRAND : 'transparent',
boxShadow: isToday && !isEndpoint ? `inset 0 0 0 1.5px ${BRAND}` : 'none',
transition: 'background-color .12s',
'&:hover': { bgcolor: disabled ? 'transparent' : isEndpoint ? '#9E0E20' : alpha(BRAND, 0.1) }
}}
>
{d.date()}
</Box>
</Box>
);
})}
</Box>
</Box>
);
}
// Click a day to set the start, click again to set the end (auto-swaps if reversed).
function RangeCalendar({ from, to, maxDate, onSelect }) {
const [view, setView] = useState(dayjs(to || from || undefined).startOf('month'));
const [anchorDate, setAnchorDate] = useState(null);
const start = from ? dayjs(from) : null;
const end = to ? dayjs(to) : null;
const max = maxDate ? dayjs(maxDate) : null;
const handleDay = (d) => {
if (!anchorDate) {
setAnchorDate(d);
onSelect(d.format(DATE_FMT), d.format(DATE_FMT));
} else {
const a = anchorDate;
const lo = d.isBefore(a) ? d : a;
const hi = d.isBefore(a) ? a : d;
onSelect(lo.format(DATE_FMT), hi.format(DATE_FMT));
setAnchorDate(null);
}
};
return (
<Box sx={{ px: 1.5, py: 1.5 }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 1 }}>
<IconButton size="small" onClick={() => setView((v) => v.subtract(1, 'month'))} sx={{ color: '#9AA0A6', p: 0.5 }}>
<ChevronLeftRoundedIcon fontSize="small" />
</IconButton>
<IconButton size="small" onClick={() => setView((v) => v.add(1, 'month'))} sx={{ color: '#9AA0A6', p: 0.5 }}>
<ChevronRightRoundedIcon fontSize="small" />
</IconButton>
</Stack>
<Stack direction="row" spacing={2}>
<MonthGrid view={view} start={start} end={end} max={max} onDay={handleDay} />
<Box sx={{ display: { xs: 'none', sm: 'block' } }}>
<MonthGrid view={view.add(1, 'month')} start={start} end={end} max={max} onDay={handleDay} />
</Box>
</Stack>
</Box>
);
}
const PRESETS = [
{ label: 'Today', days: 1 },
{ label: 'Last 7 days', days: 7 },
{ label: 'Last 30 days', days: 30 }
];
export default function DateRangePicker({ value, onChange, maxDate }) {
const today = dayjs().format(DATE_FMT);
const max = maxDate ?? today;
const [anchor, setAnchor] = useState(null);
const isMobile = useMediaQuery('(max-width:600px)');
const from = value?.from || today;
const to = value?.to || today;
const invalid = dayjs(to).isBefore(dayjs(from));
const dayCount = (() => {
const f = dayjs(from);
const t = dayjs(to);
if (!f.isValid() || !t.isValid() || t.isBefore(f)) return 1;
return t.diff(f, 'day') + 1;
})();
const applyPreset = (days) => onChange({ from: dayjs().subtract(days - 1, 'day').format(DATE_FMT), to: today });
const isPreset = (days) => to === today && from === dayjs().subtract(days - 1, 'day').format(DATE_FMT);
return (
<>
<Stack direction="row" spacing={1} alignItems="center" useFlexGap sx={{ flexWrap: 'wrap' }}>
{PRESETS.map((p) => {
const active = isPreset(p.days);
return (
<Chip
key={p.label}
label={p.label}
onClick={() => applyPreset(p.days)}
sx={{
height: 36, fontSize: '0.825rem', fontWeight: 600, borderRadius: 2,
bgcolor: active ? BRAND : '#F1F5F9', color: active ? '#fff' : '#475569',
boxShadow: active ? '0 4px 10px rgba(192,18,39,0.15)' : 'none',
'& .MuiChip-label': { px: 1.5 },
'&:hover': { bgcolor: active ? '#9E0E20' : '#E2E8F0' }
}}
/>
);
})}
<Button
onClick={(e) => setAnchor(e.currentTarget)}
variant="outlined"
startIcon={<CalendarTodayOutlinedIcon sx={{ fontSize: 16 }} />}
sx={{
height: 36, px: 1.5, borderRadius: 2, textTransform: 'none', fontWeight: 600, fontSize: '0.825rem',
color: '#334155', borderColor: invalid ? '#EF4444' : '#E2E8F0', bgcolor: '#fff',
justifyContent: 'flex-start', minWidth: { sm: 210 },
'&:hover': { borderColor: BRAND, bgcolor: alpha(BRAND, 0.02) }
}}
>
<Stack direction="row" alignItems="center" spacing={0.75} sx={{ color: '#334155' }}>
<Box component="span">{dayjs(from).format('DD MMM YYYY')}</Box>
<ArrowRightAltRoundedIcon sx={{ fontSize: 16, color: '#94A3B8' }} />
<Box component="span">{dayjs(to).format('DD MMM YYYY')}</Box>
</Stack>
</Button>
</Stack>
<Popover
open={Boolean(anchor)}
anchorEl={anchor}
onClose={() => setAnchor(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: isMobile ? 'left' : 'right' }}
transformOrigin={{ vertical: 'top', horizontal: isMobile ? 'left' : 'right' }}
marginThreshold={12}
PaperProps={{ sx: { mt: 1, borderRadius: 2.5, border: '1px solid #EEF0F2', boxShadow: '0 8px 28px rgba(0,0,0,0.10)', overflow: 'hidden', maxWidth: 'calc(100vw - 24px)' } }}
>
<Box sx={{ px: 2, pt: 1.75, pb: 0.5 }}>
<Typography sx={{ fontWeight: 700, fontSize: '0.9rem', color: '#212529' }}>Select date range</Typography>
<Typography variant="caption" sx={{ color: '#8A9099' }}>
{dayjs(from).format('DD MMM')} {dayjs(to).format('DD MMM YYYY')} · {dayCount} {dayCount === 1 ? 'day' : 'days'}
</Typography>
</Box>
<RangeCalendar from={from} to={to} maxDate={max} onSelect={(f, t) => onChange({ from: f, to: t })} />
<Divider />
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ px: 2, py: 1.25 }}>
<Button size="small" onClick={() => applyPreset(1)} sx={{ textTransform: 'none', color: '#5F6368', fontWeight: 700 }}>
Reset to today
</Button>
<Button size="small" variant="contained" onClick={() => setAnchor(null)}
sx={{ textTransform: 'none', fontWeight: 700, bgcolor: BRAND, borderRadius: 2, '&:hover': { bgcolor: '#9E0E20' } }}>
Done
</Button>
</Stack>
</Popover>
</>
);
}