feat(freight-backoffice): add DateRangePicker, replace ad-hoc from/to date filters

Adds a shadcn Popover+Calendar range picker with presets (Today, Last 7/30
days, this/last month, YTD) and Apply/Cancel. Swaps it into every shadcn-family
from-to date filter: activity log, sector reports, incoming records (EN
branch), CombinedFilterBar (13 consumers), user records, audit log — also
fixes an uncontrolled-input bug on the audit log date fields.

Mantine-based ListControls/ReportFilters left untouched (different UI kit).
This commit is contained in:
Nathnael
2026-08-13 08:33:59 +00:00
parent 29913259f6
commit 6f14cf8bbf
8 changed files with 252 additions and 214 deletions

View File

@@ -0,0 +1,197 @@
import * as React from "react";
import { CalendarIcon, ChevronDown } from "lucide-react";
import {
format,
isSameDay,
isSameYear,
startOfDay,
endOfDay,
startOfMonth,
endOfMonth,
startOfYear,
subDays,
subMonths,
} from "date-fns";
import { cn } from "@/shared/lib/utils";
import { Button } from "@/shared/common/ui/button";
import { Calendar } from "@/shared/common/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/shared/common/ui/popover";
export interface DateRangeValue {
from?: Date;
to?: Date;
}
/** Parse a `YYYY-MM-DD` string as a local calendar day (avoids the UTC-midnight timezone shift `new Date(str)` has). */
export function parseDay(value?: string | null): Date | undefined {
if (!value) return undefined;
const [y, m, d] = value.split("-").map(Number);
if (!y || !m || !d) return undefined;
return new Date(y, m - 1, d);
}
/** Format a `Date` back to `YYYY-MM-DD`, for call sites still storing plain date strings. */
export function formatDay(date?: Date): string {
return date ? format(date, "yyyy-MM-dd") : "";
}
interface DateRangePreset {
label: string;
range: () => DateRangeValue;
}
const DEFAULT_PRESETS: DateRangePreset[] = [
{ label: "Today", range: () => ({ from: startOfDay(new Date()), to: endOfDay(new Date()) }) },
{
label: "Yesterday",
range: () => ({ from: startOfDay(subDays(new Date(), 1)), to: endOfDay(subDays(new Date(), 1)) }),
},
{ label: "Last 7 days", range: () => ({ from: startOfDay(subDays(new Date(), 6)), to: endOfDay(new Date()) }) },
{ label: "Last 30 days", range: () => ({ from: startOfDay(subDays(new Date(), 29)), to: endOfDay(new Date()) }) },
{ label: "This month", range: () => ({ from: startOfMonth(new Date()), to: endOfDay(new Date()) }) },
{
label: "Last month",
range: () => ({
from: startOfMonth(subMonths(new Date(), 1)),
to: endOfMonth(subMonths(new Date(), 1)),
}),
},
{ label: "Year to date", range: () => ({ from: startOfYear(new Date()), to: endOfDay(new Date()) }) },
];
function formatRangeLabel(value?: DateRangeValue, placeholder = "Select date range") {
if (!value?.from) return placeholder;
if (!value.to || isSameDay(value.from, value.to)) {
return format(value.from, "MMM d, y");
}
if (isSameYear(value.from, value.to)) {
return `${format(value.from, "MMM d")} ${format(value.to, "MMM d, y")}`;
}
return `${format(value.from, "MMM d, y")} ${format(value.to, "MMM d, y")}`;
}
interface DateRangePickerProps {
value?: DateRangeValue;
onChange?: (range: DateRangeValue) => void;
presets?: DateRangePreset[];
placeholder?: string;
numberOfMonths?: number;
className?: string;
align?: "start" | "center" | "end";
disabled?: boolean;
}
export function DateRangePicker({
value,
onChange,
presets = DEFAULT_PRESETS,
placeholder = "Select date range",
numberOfMonths = 2,
className,
align = "start",
disabled = false,
}: DateRangePickerProps) {
const [open, setOpen] = React.useState(false);
const [draft, setDraft] = React.useState<DateRangeValue | undefined>(value);
// Reset the draft to the committed value each time the popover opens
React.useEffect(() => {
if (open) setDraft(value);
}, [open, value]);
const activePresetLabel = React.useMemo(() => {
if (!draft?.from || !draft?.to) return null;
const match = presets.find((preset) => {
const r = preset.range();
return r.from && r.to && isSameDay(r.from, draft.from!) && isSameDay(r.to, draft.to!);
});
return match?.label ?? null;
}, [draft, presets]);
function applyPreset(preset: DateRangePreset) {
const range = preset.range();
setDraft(range);
onChange?.(range);
setOpen(false);
}
function handleApply() {
if (draft?.from) onChange?.({ from: draft.from, to: draft.to ?? draft.from });
setOpen(false);
}
function handleCancel() {
setDraft(value);
setOpen(false);
}
return (
<Popover open={open && !disabled} onOpenChange={(next: boolean) => setOpen(next && !disabled)}>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
disabled={disabled}
className={cn(
"min-w-[240px] justify-start gap-2 font-normal text-slate-700 dark:text-slate-200",
!value?.from && "text-slate-500 dark:text-slate-400",
className,
)}
>
<CalendarIcon className="size-4 text-slate-500 dark:text-slate-400" />
<span className="flex-1 text-left">{formatRangeLabel(value, placeholder)}</span>
<ChevronDown className="size-4 text-slate-400" />
</Button>
</PopoverTrigger>
<PopoverContent align={align} className="w-auto p-0">
<div className="flex flex-col sm:flex-row">
<div className="flex shrink-0 flex-col gap-0.5 border-b p-2 sm:border-b-0 sm:border-r sm:w-[160px]">
{presets.map((preset) => (
<button
key={preset.label}
type="button"
onClick={() => applyPreset(preset)}
className={cn(
"rounded-md px-2.5 py-1.5 text-left text-sm font-medium text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800",
activePresetLabel === preset.label &&
"bg-primary-50 text-primary-700 hover:bg-primary-50 dark:bg-primary-900/30 dark:text-primary-300",
)}
>
{preset.label}
</button>
))}
</div>
<div className="flex flex-col">
<Calendar
mode="range"
selected={draft}
onSelect={(range: DateRangeValue | undefined) => setDraft(range ?? undefined)}
numberOfMonths={numberOfMonths}
defaultMonth={draft?.from ?? value?.from}
className="border-none shadow-none"
/>
<div className="flex items-center justify-between gap-3 border-t p-3">
<span className="text-sm text-slate-500 dark:text-slate-400">
{formatRangeLabel(draft, "No dates selected")}
</span>
<div className="flex gap-2">
<Button variant="ghost" size="sm" onClick={handleCancel}>
Cancel
</Button>
<Button size="sm" disabled={!draft?.from} onClick={handleApply}>
Apply
</Button>
</div>
</div>
</div>
</div>
</PopoverContent>
</Popover>
);
}