updates on the datepicker on the orders page and the deliveries page
This commit is contained in:
370
src/components/ds/DatePicker.jsx
Normal file
370
src/components/ds/DatePicker.jsx
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, ChevronDown } from 'lucide-react';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DatePicker — KROW design system date selector.
|
||||||
|
*
|
||||||
|
* Supports:
|
||||||
|
* - Single date ('YYYY-MM-DD') or Range ({ from, to }) or 'all'
|
||||||
|
* - Default "Today" quick preset
|
||||||
|
* - 1-click day scrubbing via prev/next steppers (< and >)
|
||||||
|
* - Interactive month grid with Today indicator and range highlights
|
||||||
|
* - Quick presets: Today, Yesterday, Last 7 Days, This Month, All Dates
|
||||||
|
*/
|
||||||
|
export function DatePicker({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
allowAll = true,
|
||||||
|
showSteppers = true,
|
||||||
|
className = '',
|
||||||
|
placeholder = 'Select date',
|
||||||
|
disabled = false,
|
||||||
|
align = 'end',
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const today = useMemo(() => dayjs().startOf('day'), []);
|
||||||
|
const todayStr = useMemo(() => today.format('YYYY-MM-DD'), [today]);
|
||||||
|
const yesterdayStr = useMemo(() => today.subtract(1, 'day').format('YYYY-MM-DD'), [today]);
|
||||||
|
|
||||||
|
// Normalize value to { from, to, isAll, isSingle }
|
||||||
|
const parsed = useMemo(() => {
|
||||||
|
if (!value || value === 'all') {
|
||||||
|
return { from: '', to: '', isAll: true, isSingle: false, isToday: false, isYesterday: false };
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const isToday = value === todayStr;
|
||||||
|
const isYesterday = value === yesterdayStr;
|
||||||
|
return { from: value, to: value, isAll: false, isSingle: true, isToday, isYesterday };
|
||||||
|
}
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
const from = value.from || '';
|
||||||
|
const to = value.to || '';
|
||||||
|
if (!from && !to) {
|
||||||
|
return { from: '', to: '', isAll: true, isSingle: false, isToday: false, isYesterday: false };
|
||||||
|
}
|
||||||
|
const isSingle = from === to;
|
||||||
|
const isToday = isSingle && from === todayStr;
|
||||||
|
const isYesterday = isSingle && from === yesterdayStr;
|
||||||
|
return { from, to, isAll: false, isSingle, isToday, isYesterday };
|
||||||
|
}
|
||||||
|
return { from: '', to: '', isAll: false, isSingle: false, isToday: false, isYesterday: false };
|
||||||
|
}, [value, todayStr, yesterdayStr]);
|
||||||
|
|
||||||
|
// Current calendar viewing month
|
||||||
|
const [viewMonth, setViewMonth] = useState(() => {
|
||||||
|
if (parsed.from && dayjs(parsed.from).isValid()) {
|
||||||
|
return dayjs(parsed.from).startOf('month');
|
||||||
|
}
|
||||||
|
return today.startOf('month');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep viewMonth synced when value changes and popover opens
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
if (parsed.from && dayjs(parsed.from).isValid()) {
|
||||||
|
setViewMonth(dayjs(parsed.from).startOf('month'));
|
||||||
|
} else {
|
||||||
|
setViewMonth(today.startOf('month'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [open, parsed.from, today]);
|
||||||
|
|
||||||
|
// Label text for trigger chip
|
||||||
|
const displayLabel = useMemo(() => {
|
||||||
|
if (parsed.isAll) return 'All Dates';
|
||||||
|
if (parsed.isToday) return 'Today';
|
||||||
|
if (parsed.isYesterday) return 'Yesterday';
|
||||||
|
if (parsed.isSingle && parsed.from) {
|
||||||
|
const d = dayjs(parsed.from);
|
||||||
|
return d.isValid() ? d.format('DD MMM YYYY') : parsed.from;
|
||||||
|
}
|
||||||
|
if (parsed.from && parsed.to) {
|
||||||
|
const d1 = dayjs(parsed.from);
|
||||||
|
const d2 = dayjs(parsed.to);
|
||||||
|
if (d1.isValid() && d2.isValid()) {
|
||||||
|
return `${d1.format('DD MMM')} → ${d2.format('DD MMM YYYY')}`;
|
||||||
|
}
|
||||||
|
return `${parsed.from} → ${parsed.to}`;
|
||||||
|
}
|
||||||
|
if (parsed.from) {
|
||||||
|
const d = dayjs(parsed.from);
|
||||||
|
return d.isValid() ? d.format('DD MMM YYYY') : parsed.from;
|
||||||
|
}
|
||||||
|
return placeholder;
|
||||||
|
}, [parsed, placeholder]);
|
||||||
|
|
||||||
|
// Commit a date or preset
|
||||||
|
const commit = (nextVal) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(nextVal);
|
||||||
|
}
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Day scrubbing (< and >)
|
||||||
|
const handlePrevDay = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (disabled) return;
|
||||||
|
const baseDate = parsed.from && dayjs(parsed.from).isValid() ? dayjs(parsed.from) : today;
|
||||||
|
const prev = baseDate.subtract(1, 'day').format('YYYY-MM-DD');
|
||||||
|
if (typeof value === 'object' && !parsed.isSingle && value.to) {
|
||||||
|
commit({ from: prev, to: prev });
|
||||||
|
} else {
|
||||||
|
commit(prev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextDay = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (disabled) return;
|
||||||
|
const baseDate = parsed.from && dayjs(parsed.from).isValid() ? dayjs(parsed.from) : today;
|
||||||
|
const next = baseDate.add(1, 'day').format('YYYY-MM-DD');
|
||||||
|
if (typeof value === 'object' && !parsed.isSingle && value.to) {
|
||||||
|
commit({ from: next, to: next });
|
||||||
|
} else {
|
||||||
|
commit(next);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 6x7 Grid generation for viewMonth
|
||||||
|
const monthStart = viewMonth.startOf('month');
|
||||||
|
const gridStart = monthStart.subtract(monthStart.day(), 'day');
|
||||||
|
const cells = useMemo(() => {
|
||||||
|
return Array.from({ length: 42 }, (_, i) => gridStart.add(i, 'day'));
|
||||||
|
}, [gridStart]);
|
||||||
|
|
||||||
|
const WEEKDAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
|
||||||
|
|
||||||
|
const isSelected = (day) => {
|
||||||
|
if (parsed.isAll) return false;
|
||||||
|
const s = day.format('YYYY-MM-DD');
|
||||||
|
if (parsed.from && parsed.to) {
|
||||||
|
return s >= parsed.from && s <= parsed.to;
|
||||||
|
}
|
||||||
|
return s === parsed.from;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isRangeEndpoint = (day) => {
|
||||||
|
if (parsed.isAll) return false;
|
||||||
|
const s = day.format('YYYY-MM-DD');
|
||||||
|
return s === parsed.from || s === parsed.to;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<div className={cn('inline-flex items-center rounded-lg border border-border bg-card shadow-2xs transition-colors', className)}>
|
||||||
|
{showSteppers && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handlePrevDay}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-label="Previous day"
|
||||||
|
title="Previous day"
|
||||||
|
className="flex h-9 w-8 items-center justify-center rounded-l-lg text-ink-3 hover:bg-muted/70 hover:text-ink-1 disabled:opacity-40 transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
'flex h-9 items-center gap-2 px-2.5 text-xs font-medium text-ink-1 hover:bg-muted/50 transition-colors',
|
||||||
|
!showSteppers && 'rounded-lg',
|
||||||
|
showSteppers && 'border-x border-border/60',
|
||||||
|
open && 'bg-muted/60'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarIcon className="h-3.5 w-3.5 text-ink-3 shrink-0" />
|
||||||
|
<span className="truncate flex items-center gap-1.5">
|
||||||
|
{displayLabel}
|
||||||
|
{parsed.isToday && (
|
||||||
|
<span className="rounded-full bg-emerald-500/10 px-1.5 py-0.2 text-[10px] font-semibold text-emerald-600 dark:text-emerald-400">
|
||||||
|
Today
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className={cn('h-3.5 w-3.5 text-ink-4 shrink-0 transition-transform duration-150', open && 'rotate-180')} />
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
{showSteppers && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleNextDay}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-label="Next day"
|
||||||
|
title="Next day"
|
||||||
|
className="flex h-9 w-8 items-center justify-center rounded-r-lg text-ink-3 hover:bg-muted/70 hover:text-ink-1 disabled:opacity-40 transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PopoverContent align={align} className="w-[300px] p-3 shadow-xl rounded-xl border border-border bg-card z-50">
|
||||||
|
{/* Month Navigation */}
|
||||||
|
<div className="flex items-center justify-between gap-1 pb-2 border-b border-border/60">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setViewMonth((m) => m.subtract(1, 'month'))}
|
||||||
|
className="flex h-7 w-7 items-center justify-center rounded-md text-ink-2 hover:bg-muted hover:text-ink-1 transition-colors"
|
||||||
|
aria-label="Previous month"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<span className="text-xs font-semibold text-ink-1">
|
||||||
|
{viewMonth.format('MMMM YYYY')}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setViewMonth((m) => m.add(1, 'month'))}
|
||||||
|
className="flex h-7 w-7 items-center justify-center rounded-md text-ink-2 hover:bg-muted hover:text-ink-1 transition-colors"
|
||||||
|
aria-label="Next month"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Weekdays */}
|
||||||
|
<div className="grid grid-cols-7 gap-1 pt-2 text-center text-[10px] font-semibold text-ink-4 uppercase">
|
||||||
|
{WEEKDAYS.map((w) => (
|
||||||
|
<div key={w} className="py-0.5">{w}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar Grid */}
|
||||||
|
<div className="grid grid-cols-7 gap-1 pt-1">
|
||||||
|
{cells.map((d) => {
|
||||||
|
const dateStr = d.format('YYYY-MM-DD');
|
||||||
|
const inMonth = d.month() === viewMonth.month();
|
||||||
|
const selected = isSelected(d);
|
||||||
|
const isTodayCell = dateStr === todayStr;
|
||||||
|
const endpoint = isRangeEndpoint(d);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={dateStr}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (typeof value === 'object' && !parsed.isSingle && value.to) {
|
||||||
|
commit({ from: dateStr, to: dateStr });
|
||||||
|
} else {
|
||||||
|
commit(dateStr);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'flex h-7 w-full items-center justify-center rounded-md text-xs font-medium transition-colors relative',
|
||||||
|
!inMonth && 'text-ink-4/40 hover:text-ink-2',
|
||||||
|
inMonth && !selected && 'text-ink-1 hover:bg-muted',
|
||||||
|
selected && !endpoint && 'bg-primary/15 text-primary rounded-none',
|
||||||
|
endpoint && 'bg-primary text-primary-foreground font-bold shadow-2xs hover:bg-primary/90',
|
||||||
|
isTodayCell && !selected && 'ring-1 ring-inset ring-emerald-500 font-semibold text-emerald-600 dark:text-emerald-400'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{d.date()}
|
||||||
|
{isTodayCell && !selected && (
|
||||||
|
<span className="absolute bottom-0.5 h-1 w-1 rounded-full bg-emerald-500" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Presets */}
|
||||||
|
<div className="mt-3 pt-2.5 border-t border-border/60 flex flex-wrap gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => commit(todayStr)}
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1 text-[11px] font-medium rounded-md transition-colors',
|
||||||
|
parsed.isToday ? 'bg-primary text-primary-foreground font-semibold' : 'bg-muted/70 text-ink-2 hover:bg-muted hover:text-ink-1'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => commit(yesterdayStr)}
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1 text-[11px] font-medium rounded-md transition-colors',
|
||||||
|
parsed.isYesterday ? 'bg-primary text-primary-foreground font-semibold' : 'bg-muted/70 text-ink-2 hover:bg-muted hover:text-ink-1'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Yesterday
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const from = today.subtract(6, 'day').format('YYYY-MM-DD');
|
||||||
|
const to = todayStr;
|
||||||
|
commit(typeof value === 'object' ? { from, to } : { from, to });
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1 text-[11px] font-medium rounded-md transition-colors',
|
||||||
|
parsed.from === today.subtract(6, 'day').format('YYYY-MM-DD') && parsed.to === todayStr
|
||||||
|
? 'bg-primary text-primary-foreground font-semibold'
|
||||||
|
: 'bg-muted/70 text-ink-2 hover:bg-muted hover:text-ink-1'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Last 7 Days
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
const from = today.startOf('month').format('YYYY-MM-DD');
|
||||||
|
const to = todayStr;
|
||||||
|
commit(typeof value === 'object' ? { from, to } : { from, to });
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1 text-[11px] font-medium rounded-md transition-colors',
|
||||||
|
parsed.from === today.startOf('month').format('YYYY-MM-DD') && parsed.to === todayStr
|
||||||
|
? 'bg-primary text-primary-foreground font-semibold'
|
||||||
|
: 'bg-muted/70 text-ink-2 hover:bg-muted hover:text-ink-1'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
This Month
|
||||||
|
</button>
|
||||||
|
{allowAll && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => commit('all')}
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-1 text-[11px] font-medium rounded-md transition-colors ml-auto',
|
||||||
|
parsed.isAll ? 'bg-primary text-primary-foreground font-semibold' : 'bg-muted/70 text-ink-2 hover:bg-muted hover:text-ink-1'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
All Dates
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DatePicker.propTypes = {
|
||||||
|
value: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.shape({
|
||||||
|
from: PropTypes.string,
|
||||||
|
to: PropTypes.string,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
allowAll: PropTypes.bool,
|
||||||
|
showSteppers: PropTypes.bool,
|
||||||
|
className: PropTypes.string,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
align: PropTypes.oneOf(['start', 'center', 'end']),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DatePicker;
|
||||||
@@ -29,6 +29,7 @@ export { Input, inputVariants } from '@/components/ui/input';
|
|||||||
export { Textarea } from '@/components/ui/textarea';
|
export { Textarea } from '@/components/ui/textarea';
|
||||||
export { Label } from '@/components/ui/label';
|
export { Label } from '@/components/ui/label';
|
||||||
export { SearchInput } from './SearchInput';
|
export { SearchInput } from './SearchInput';
|
||||||
|
export { DatePicker } from './DatePicker';
|
||||||
export { MultiSelect } from './MultiSelect';
|
export { MultiSelect } from './MultiSelect';
|
||||||
export { Toggle, SegmentedToggle } from './Toggle';
|
export { Toggle, SegmentedToggle } from './Toggle';
|
||||||
export { Checkbox } from '@/components/ui/checkbox';
|
export { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { Ban, Bell, Bike, FileSpreadsheet, Package, PackageSearch, Truck } from 'lucide-react';
|
import { Ban, Bell, Bike, FileSpreadsheet, Package, PackageSearch, Truck } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
Alert, Button, DataTable, Drawer, EmptyState, Field, IconButton, Input,
|
Alert, Button, DataTable, DatePicker, Drawer, EmptyState, Field, IconButton, Input,
|
||||||
PageHeader, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Modal,
|
PageHeader, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Modal,
|
||||||
Stack, StatusBadge, Surface, Tabs, Textarea, ZoneSelector,
|
Stack, StatusBadge, Surface, Tabs, Textarea, ZoneSelector,
|
||||||
} from '@/components/ds';
|
} from '@/components/ds';
|
||||||
@@ -62,7 +63,8 @@ const SETTABLE_STATUSES = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function Deliveries() {
|
export default function Deliveries() {
|
||||||
const [range, setRange] = useState({ from: '', to: '' });
|
const todayStr = dayjs().format('YYYY-MM-DD');
|
||||||
|
const [selectedDate, setSelectedDate] = useState(todayStr);
|
||||||
const [tab, setTab] = useState('all');
|
const [tab, setTab] = useState('all');
|
||||||
const [batch, setBatch] = useState('all');
|
const [batch, setBatch] = useState('all');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@@ -80,11 +82,17 @@ export default function Deliveries() {
|
|||||||
const [cancelRow, setCancelRow] = useState(null);
|
const [cancelRow, setCancelRow] = useState(null);
|
||||||
const [cancelReason, setCancelReason] = useState('');
|
const [cancelReason, setCancelReason] = useState('');
|
||||||
|
|
||||||
|
const dateParams = useMemo(() => {
|
||||||
|
if (!selectedDate || selectedDate === 'all') return { from: '', to: '' };
|
||||||
|
if (typeof selectedDate === 'string') return { from: selectedDate, to: selectedDate };
|
||||||
|
return { from: selectedDate.from || '', to: selectedDate.to || '' };
|
||||||
|
}, [selectedDate]);
|
||||||
|
|
||||||
const { data, isLoading, isFetching } = useDeliveries({
|
const { data, isLoading, isFetching } = useDeliveries({
|
||||||
page,
|
page,
|
||||||
pageSize: 100,
|
pageSize: 100,
|
||||||
from: range.from,
|
from: dateParams.from,
|
||||||
to: range.to,
|
to: dateParams.to,
|
||||||
});
|
});
|
||||||
const { data: counts } = useDeliveryCounts();
|
const { data: counts } = useDeliveryCounts();
|
||||||
const { data: riders = [] } = useRiderOptions();
|
const { data: riders = [] } = useRiderOptions();
|
||||||
@@ -195,7 +203,7 @@ export default function Deliveries() {
|
|||||||
means anything. */
|
means anything. */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}, [tab, batch, tenantId, riderId, debouncedSearch, range.from, range.to]);
|
}, [tab, batch, tenantId, riderId, debouncedSearch, dateParams.from, dateParams.to]);
|
||||||
|
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -375,30 +383,23 @@ export default function Deliveries() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
icon={Truck}
|
icon={Truck}
|
||||||
title="Deliveries"
|
title="Deliveries"
|
||||||
subtitle={`${counts?.total ?? 0} dispatched · ${
|
subtitle={`${batchRows.length} dispatched · ${
|
||||||
!range.from && !range.to
|
!dateParams.from && !dateParams.to
|
||||||
? 'All dates'
|
? 'All dates'
|
||||||
: range.from === range.to
|
: selectedDate === todayStr
|
||||||
? range.from
|
? 'Today'
|
||||||
: `${range.from || '…'} → ${range.to || '…'}`
|
: selectedDate === dayjs().subtract(1, 'day').format('YYYY-MM-DD')
|
||||||
|
? 'Yesterday'
|
||||||
|
: dateParams.from === dateParams.to
|
||||||
|
? dayjs(dateParams.from).format('DD MMM YYYY')
|
||||||
|
: `${dayjs(dateParams.from).format('DD MMM')} → ${dayjs(dateParams.to).format('DD MMM YYYY')}`
|
||||||
}`}
|
}`}
|
||||||
actions={
|
actions={
|
||||||
<>
|
<DatePicker
|
||||||
<Input
|
value={selectedDate}
|
||||||
type="date"
|
onChange={setSelectedDate}
|
||||||
value={range.from}
|
allowAll
|
||||||
onChange={(e) => setRange((current) => ({ ...current, from: e.target.value }))}
|
/>
|
||||||
aria-label="From date"
|
|
||||||
className="w-40"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="date"
|
|
||||||
value={range.to}
|
|
||||||
onChange={(e) => setRange((current) => ({ ...current, to: e.target.value }))}
|
|
||||||
aria-label="To date"
|
|
||||||
className="w-40"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -418,7 +419,7 @@ export default function Deliveries() {
|
|||||||
actions={
|
actions={
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => exportRows(rows, exportColumns, `deliveries-${tab}-${range.from || 'all'}`)}
|
onClick={() => exportRows(rows, exportColumns, `deliveries-${tab}-${dateParams.from || 'all'}`)}
|
||||||
disabled={rows.length === 0}
|
disabled={rows.length === 0}
|
||||||
>
|
>
|
||||||
<FileSpreadsheet className="mr-1.5 h-4 w-4" /> Export
|
<FileSpreadsheet className="mr-1.5 h-4 w-4" /> Export
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
|
|||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { Ban, Bike, FileSpreadsheet, MapPin, Package, Plus, Sparkles, Truck, Upload, UserCheck } from 'lucide-react';
|
import { Ban, Bike, FileSpreadsheet, MapPin, Package, Plus, Sparkles, Truck, Upload, UserCheck } from 'lucide-react';
|
||||||
import { Button, ConfirmModal, DataTable, EmptyState, Modal, PageHeader, SearchInput, Stack, StatusBadge, Tabs, ZoneSelector } from '@/components/ds';
|
import { Button, ConfirmModal, DataTable, DatePicker, EmptyState, Modal, PageHeader, SearchInput, Stack, StatusBadge, Tabs, ZoneSelector } from '@/components/ds';
|
||||||
import { useBulkCancelBookings, useCustomers, useRiderOptions } from '@/lib/doormileHooks';
|
import { useBulkCancelBookings, useCustomers, useRiderOptions } from '@/lib/doormileHooks';
|
||||||
import { useZone } from '@/lib/ZoneContext';
|
import { useZone } from '@/lib/ZoneContext';
|
||||||
import { createAutomationDeliveries } from '@/api/doormile';
|
import { createAutomationDeliveries } from '@/api/doormile';
|
||||||
@@ -75,6 +75,8 @@ export default function Orders() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const todayStr = dayjs().format('YYYY-MM-DD');
|
||||||
|
const [selectedDate, setSelectedDate] = useState(todayStr);
|
||||||
const [tab, setTab] = useState('pending');
|
const [tab, setTab] = useState('pending');
|
||||||
const [search, setSearch] = useState(searchParams.get('search') || '');
|
const [search, setSearch] = useState(searchParams.get('search') || '');
|
||||||
const debouncedSearch = useDebouncedValue(search);
|
const debouncedSearch = useDebouncedValue(search);
|
||||||
@@ -115,9 +117,28 @@ export default function Orders() {
|
|||||||
});
|
});
|
||||||
}, [bookings]);
|
}, [bookings]);
|
||||||
|
|
||||||
|
const dateFilteredBookings = useMemo(() => {
|
||||||
|
if (!selectedDate || selectedDate === 'all') return sortedBookings;
|
||||||
|
return sortedBookings.filter((booking) => {
|
||||||
|
const at = parseDoormileTimestamp(booking.createdat);
|
||||||
|
if (!at.isValid()) return false;
|
||||||
|
if (typeof selectedDate === 'string') {
|
||||||
|
return at.format('YYYY-MM-DD') === selectedDate;
|
||||||
|
}
|
||||||
|
if (selectedDate.from && selectedDate.to) {
|
||||||
|
const day = at.format('YYYY-MM-DD');
|
||||||
|
return day >= selectedDate.from && day <= selectedDate.to;
|
||||||
|
}
|
||||||
|
if (selectedDate.from) {
|
||||||
|
return at.format('YYYY-MM-DD') >= selectedDate.from;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [sortedBookings, selectedDate]);
|
||||||
|
|
||||||
const zoneBookings = useMemo(() => {
|
const zoneBookings = useMemo(() => {
|
||||||
return sortedBookings.filter(matchesZone);
|
return dateFilteredBookings.filter(matchesZone);
|
||||||
}, [sortedBookings, matchesZone]);
|
}, [dateFilteredBookings, matchesZone]);
|
||||||
|
|
||||||
const customerMap = useMemo(() => {
|
const customerMap = useMemo(() => {
|
||||||
const map = new Map();
|
const map = new Map();
|
||||||
@@ -158,7 +179,7 @@ export default function Orders() {
|
|||||||
— the assign buttons act on the selection, not on what is on screen. */
|
— the assign buttons act on the selection, not on what is on screen. */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedIds([]);
|
setSelectedIds([]);
|
||||||
}, [tab, selectedZoneId]);
|
}, [tab, selectedZoneId, selectedDate]);
|
||||||
|
|
||||||
const selectedOrders = useMemo(
|
const selectedOrders = useMemo(
|
||||||
() => rows.filter((row) => selectedIds.includes(row.bookingid)),
|
() => rows.filter((row) => selectedIds.includes(row.bookingid)),
|
||||||
@@ -395,6 +416,11 @@ export default function Orders() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-1 flex-wrap items-center justify-end gap-2 sm:flex-none">
|
<div className="flex min-w-0 flex-1 flex-wrap items-center justify-end gap-2 sm:flex-none">
|
||||||
|
<DatePicker
|
||||||
|
value={selectedDate}
|
||||||
|
onChange={setSelectedDate}
|
||||||
|
allowAll
|
||||||
|
/>
|
||||||
<ZoneSelector />
|
<ZoneSelector />
|
||||||
<SearchInput
|
<SearchInput
|
||||||
value={search}
|
value={search}
|
||||||
@@ -409,7 +435,10 @@ export default function Orders() {
|
|||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => exportRows(rows, exportColumns, `orders-${tab}-${dayjs().format('YYYYMMDD')}`)}
|
onClick={() => {
|
||||||
|
const dateTag = typeof selectedDate === 'string' ? selectedDate : selectedDate?.from || 'all';
|
||||||
|
exportRows(rows, exportColumns, `orders-${tab}-${dateTag}`);
|
||||||
|
}}
|
||||||
disabled={rows.length === 0}
|
disabled={rows.length === 0}
|
||||||
>
|
>
|
||||||
<FileSpreadsheet className="mr-1.5 h-4 w-4" /> Export
|
<FileSpreadsheet className="mr-1.5 h-4 w-4" /> Export
|
||||||
@@ -439,6 +468,11 @@ export default function Orders() {
|
|||||||
{rows.length === (counts[tab] ?? 0)
|
{rows.length === (counts[tab] ?? 0)
|
||||||
? `${counts[tab] ?? 0} orders`
|
? `${counts[tab] ?? 0} orders`
|
||||||
: `${rows.length} of ${counts[tab] ?? 0} orders`}
|
: `${rows.length} of ${counts[tab] ?? 0} orders`}
|
||||||
|
{selectedDate && selectedDate !== 'all' && (
|
||||||
|
<span className="font-medium text-ink-2">
|
||||||
|
{' '}· Date: {selectedDate === todayStr ? 'Today' : typeof selectedDate === 'string' ? selectedDate : `${selectedDate.from} → ${selectedDate.to}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{!isAllZones && (
|
{!isAllZones && (
|
||||||
<span className="font-medium text-ink-2"> · Zone: {selectedZone.hubname}</span>
|
<span className="font-medium text-ink-2"> · Zone: {selectedZone.hubname}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user