feat(freight-backoffice): consolidate Mantine from/to date filters into range pickers

Replaces every remaining split from/to Mantine DateInput/DatePickerInput pair
with a single DatePickerInput type="range", sharing one preset list (Today,
Last 7/30 days, this/last month, YTD) via getDateRangePresets(). Covers
ListControls (18 consumers), FleetResourcePage, ContractRequestsPage,
BookingRequestsPage (created + scheduled ranges), WagonCancellationsPage,
BatchBoardPage, ClearanceDocumentsPage, ShipmentRequestsPage.

Native Mantine range picker, not the shadcn DateRangePicker, to match each
page's existing design system instead of clashing with it.

Reports date-range filter (ReportFilters.tsx) intentionally left untouched.
This commit is contained in:
Nathnael
2026-08-13 08:45:29 +00:00
parent efc5a24380
commit 58b47318e9
9 changed files with 146 additions and 176 deletions

View File

@@ -2,6 +2,7 @@ import { Button, Group, TextInput } from "@mantine/core";
import { DatePickerInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { Search, X } from "lucide-react"; import { Search, X } from "lucide-react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { getDateRangePresets } from "./dateRangePresets";
export interface ListControlsProps { export interface ListControlsProps {
search: string; search: string;
@@ -54,28 +55,19 @@ const ListControls = ({
)} )}
{showDateRange && ( {showDateRange && (
<> <DatePickerInput
<DatePickerInput type="range"
label={dateLabel ? `${dateLabel} from` : "From"} label={dateLabel ?? "Date range"}
placeholder="Any" placeholder="Any"
value={dateFrom} value={[dateFrom, dateTo]}
onChange={onDateFromChange} onChange={([from, to]) => {
// Cannot start after it ends — the picker refuses the invalid range onDateFromChange(from);
// instead of silently returning nothing. onDateToChange(to);
maxDate={dateTo ?? undefined} }}
clearable presets={getDateRangePresets()}
w={150} clearable
/> w={230}
<DatePickerInput />
label={dateLabel ? `${dateLabel} to` : "To"}
placeholder="Any"
value={dateTo}
onChange={onDateToChange}
minDate={dateFrom ?? undefined}
clearable
w={150}
/>
</>
)} )}
{children} {children}

View File

@@ -0,0 +1,41 @@
import {
format,
startOfDay,
endOfDay,
startOfMonth,
endOfMonth,
startOfYear,
subDays,
subMonths,
} from "date-fns";
import type { DatePickerPreset } from "@mantine/dates";
const iso = (date: Date) => format(date, "yyyy-MM-dd");
/**
* Shared "Today / Last 7 days / …" presets for every Mantine
* `<DatePickerInput type="range" presets={getDateRangePresets()} />` in the app,
* so every from/to filter offers the same shortcuts. Computed fresh per call
* (not a module-level constant) so "Today" stays today.
*/
export function getDateRangePresets(): DatePickerPreset<"range">[] {
const today = new Date();
return [
{ label: "Today", value: [iso(startOfDay(today)), iso(endOfDay(today))] },
{
label: "Yesterday",
value: [iso(startOfDay(subDays(today, 1))), iso(endOfDay(subDays(today, 1)))],
},
{ label: "Last 7 days", value: [iso(startOfDay(subDays(today, 6))), iso(endOfDay(today))] },
{ label: "Last 30 days", value: [iso(startOfDay(subDays(today, 29))), iso(endOfDay(today))] },
{ label: "This month", value: [iso(startOfMonth(today)), iso(endOfDay(today))] },
{
label: "Last month",
value: [
iso(startOfMonth(subMonths(today, 1))),
iso(endOfMonth(subMonths(today, 1))),
],
},
{ label: "Year to date", value: [iso(startOfYear(today)), iso(endOfDay(today))] },
];
}

View File

@@ -13,7 +13,8 @@ import {
Text, Text,
TextInput, TextInput,
} from "@mantine/core"; } from "@mantine/core";
import { DateInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { getDateRangePresets } from "@/components/common/dateRangePresets";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import { import {
AlertTriangle, AlertTriangle,
@@ -764,53 +765,33 @@ export default function BookingRequestsPage() {
radius="lg" radius="lg"
style={{ minWidth: 140 }} style={{ minWidth: 140 }}
/> />
<DateInput <DatePickerInput
placeholder="Created from" type="range"
value={createdFrom} placeholder="Created date range"
onChange={(v) => { value={[createdFrom, createdTo]}
setCreatedFrom(v ? new Date(v) : null); onChange={([from, to]) => {
setCreatedFrom(from ? new Date(from) : null);
setCreatedTo(to ? new Date(to) : null);
resetPage(); resetPage();
}} }}
maxDate={createdTo ?? undefined} presets={getDateRangePresets()}
clearable clearable
radius="lg" radius="lg"
style={{ minWidth: 140 }} style={{ minWidth: 220 }}
/> />
<DateInput <DatePickerInput
placeholder="Created to" type="range"
value={createdTo} placeholder="Scheduled date range"
onChange={(v) => { value={[scheduledFrom, scheduledTo]}
setCreatedTo(v ? new Date(v) : null); onChange={([from, to]) => {
setScheduledFrom(from ? new Date(from) : null);
setScheduledTo(to ? new Date(to) : null);
resetPage(); resetPage();
}} }}
minDate={createdFrom ?? undefined} presets={getDateRangePresets()}
clearable clearable
radius="lg" radius="lg"
style={{ minWidth: 140 }} style={{ minWidth: 230 }}
/>
<DateInput
placeholder="Scheduled from"
value={scheduledFrom}
onChange={(v) => {
setScheduledFrom(v ? new Date(v) : null);
resetPage();
}}
maxDate={scheduledTo ?? undefined}
clearable
radius="lg"
style={{ minWidth: 150 }}
/>
<DateInput
placeholder="Scheduled to"
value={scheduledTo}
onChange={(v) => {
setScheduledTo(v ? new Date(v) : null);
resetPage();
}}
minDate={scheduledFrom ?? undefined}
clearable
radius="lg"
style={{ minWidth: 150 }}
/> />
{activeFilterCount > 0 ? ( {activeFilterCount > 0 ? (
<Button <Button

View File

@@ -11,7 +11,8 @@ import {
Text, Text,
TextInput, TextInput,
} from "@mantine/core"; } from "@mantine/core";
import { DateInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { getDateRangePresets } from "@/components/common/dateRangePresets";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { Search, XCircle } from "lucide-react"; import { Search, XCircle } from "lucide-react";
@@ -304,29 +305,19 @@ export default function WagonCancellationsPage() {
w={190} w={190}
radius="md" radius="md"
/> />
<DateInput <DatePickerInput
placeholder="From" type="range"
value={from} placeholder="Date range"
onChange={(v) => { value={[from, to]}
setFrom(v ? new Date(v) : null); onChange={([newFrom, newTo]) => {
setFrom(newFrom ? new Date(newFrom) : null);
setTo(newTo ? new Date(newTo) : null);
resetPage(); resetPage();
}} }}
maxDate={to ?? undefined} presets={getDateRangePresets()}
clearable clearable
radius="md" radius="md"
style={{ minWidth: 140 }} style={{ minWidth: 220 }}
/>
<DateInput
placeholder="To"
value={to}
onChange={(v) => {
setTo(v ? new Date(v) : null);
resetPage();
}}
minDate={from ?? undefined}
clearable
radius="md"
style={{ minWidth: 140 }}
/> />
<Button <Button
variant="subtle" variant="subtle"

View File

@@ -10,7 +10,8 @@ import {
TextInput, TextInput,
ThemeIcon, ThemeIcon,
} from "@mantine/core"; } from "@mantine/core";
import { DateInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { getDateRangePresets } from "@/components/common/dateRangePresets";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { FileText, Inbox, RefreshCw, Search, User, X } from "lucide-react"; import { FileText, Inbox, RefreshCw, Search, User, X } from "lucide-react";
@@ -348,31 +349,20 @@ export default function ClearanceDocumentsPage() {
style={{ minWidth: 140 }} style={{ minWidth: 140 }}
aria-label="Filter by ownership" aria-label="Filter by ownership"
/> />
<DateInput <DatePickerInput
placeholder="Created from" type="range"
value={createdFrom} placeholder="Created date range"
onChange={(v) => { value={[createdFrom, createdTo]}
setCreatedFrom(v ? new Date(v) : null); onChange={([from, to]) => {
setCreatedFrom(from ? new Date(from) : null);
setCreatedTo(to ? new Date(to) : null);
resetPage(); resetPage();
}} }}
maxDate={createdTo ?? undefined} presets={getDateRangePresets()}
clearable clearable
radius="lg" radius="lg"
style={{ minWidth: 140 }} style={{ minWidth: 220 }}
aria-label="Created from" aria-label="Created date range"
/>
<DateInput
placeholder="Created to"
value={createdTo}
onChange={(v) => {
setCreatedTo(v ? new Date(v) : null);
resetPage();
}}
minDate={createdFrom ?? undefined}
clearable
radius="lg"
style={{ minWidth: 140 }}
aria-label="Created to"
/> />
</Group> </Group>
</Box> </Box>

View File

@@ -13,7 +13,8 @@ import {
TextInput, TextInput,
ThemeIcon, ThemeIcon,
} from "@mantine/core"; } from "@mantine/core";
import { DateInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { getDateRangePresets } from "@/components/common/dateRangePresets";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import { import {
AlertTriangle, AlertTriangle,
@@ -613,31 +614,20 @@ export default function ContractRequestsPage() {
style={{ minWidth: 140 }} style={{ minWidth: 140 }}
aria-label="Filter by payment currency" aria-label="Filter by payment currency"
/> />
<DateInput <DatePickerInput
placeholder="Created from" type="range"
value={createdFrom} placeholder="Created date range"
onChange={(v) => { value={[createdFrom, createdTo]}
setCreatedFrom(v ? new Date(v) : null); onChange={([from, to]) => {
setCreatedFrom(from ? new Date(from) : null);
setCreatedTo(to ? new Date(to) : null);
resetPage(); resetPage();
}} }}
maxDate={createdTo ?? undefined} presets={getDateRangePresets()}
clearable clearable
radius="lg" radius="lg"
style={{ minWidth: 140 }} style={{ minWidth: 220 }}
aria-label="Created from" aria-label="Created date range"
/>
<DateInput
placeholder="Created to"
value={createdTo}
onChange={(v) => {
setCreatedTo(v ? new Date(v) : null);
resetPage();
}}
minDate={createdFrom ?? undefined}
clearable
radius="lg"
style={{ minWidth: 140 }}
aria-label="Created to"
/> />
{activeFilterCount > 0 ? ( {activeFilterCount > 0 ? (
<Button <Button

View File

@@ -17,7 +17,8 @@ import {
Textarea, Textarea,
TextInput, TextInput,
} from "@mantine/core"; } from "@mantine/core";
import { DateInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { getDateRangePresets } from "@/components/common/dateRangePresets";
import { import {
ArrowUpDown, ArrowUpDown,
FilterX, FilterX,
@@ -460,25 +461,19 @@ export default function ShipmentRequestsPage() {
allowDeselect={false} allowDeselect={false}
aria-label="Cargo type" aria-label="Cargo type"
/> />
<DateInput <DatePickerInput
type="range"
radius="md" radius="md"
w={150} w={230}
placeholder="Preferred from" placeholder="Preferred date range"
value={preferredFrom} value={[preferredFrom, preferredTo]}
onChange={(v) => setPreferredFrom(v ? new Date(v) : null)} onChange={([from, to]) => {
maxDate={preferredTo ?? undefined} setPreferredFrom(from ? new Date(from) : null);
setPreferredTo(to ? new Date(to) : null);
}}
presets={getDateRangePresets()}
clearable clearable
aria-label="Preferred date from" aria-label="Preferred date range"
/>
<DateInput
radius="md"
w={150}
placeholder="Preferred to"
value={preferredTo}
onChange={(v) => setPreferredTo(v ? new Date(v) : null)}
minDate={preferredFrom ?? undefined}
clearable
aria-label="Preferred date to"
/> />
<Select <Select
radius="md" radius="md"

View File

@@ -1,6 +1,7 @@
import type { ColumnDef } from "@edr/ui-common"; import type { ColumnDef } from "@edr/ui-common";
import { Box, Button, Card, Container, Group, Modal, Select, Stack, Text, TextInput, Title } from "@mantine/core"; import { Box, Button, Card, Container, Group, Modal, Select, Stack, Text, TextInput, Title } from "@mantine/core";
import { DatePickerInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { getDateRangePresets } from "@/components/common/dateRangePresets";
import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query"; import { keepPreviousData, useMutation, useQuery } from "@tanstack/react-query";
import { api } from "@/services/api"; import { api } from "@/services/api";
@@ -613,26 +614,19 @@ const FleetResourcePage = () => {
filters={ filters={
<Group gap="sm" wrap="wrap" align="center"> <Group gap="sm" wrap="wrap" align="center">
<DatePickerInput <DatePickerInput
aria-label="Created from" type="range"
placeholder="Created from" aria-label="Created date range"
value={dateFrom} placeholder="Created date range"
onChange={setDateFrom} value={[dateFrom, dateTo]}
maxDate={dateTo ?? undefined} onChange={([from, to]) => {
setDateFrom(from);
setDateTo(to);
}}
presets={getDateRangePresets()}
clearable clearable
size="sm" size="sm"
radius="lg" radius="lg"
w={160} w={240}
/>
<DatePickerInput
aria-label="Created to"
placeholder="Created to"
value={dateTo}
onChange={setDateTo}
minDate={dateFrom ?? undefined}
clearable
size="sm"
radius="lg"
w={160}
/> />
{listFilterSelects ? ( {listFilterSelects ? (
<Group gap="sm" wrap="wrap" align="center"> <Group gap="sm" wrap="wrap" align="center">

View File

@@ -19,7 +19,8 @@ import {
ThemeIcon, ThemeIcon,
Tooltip, Tooltip,
} from "@mantine/core"; } from "@mantine/core";
import { DateInput } from "@mantine/dates"; import { DatePickerInput } from "@mantine/dates";
import { getDateRangePresets } from "@/components/common/dateRangePresets";
import { useDebouncedValue } from "@mantine/hooks"; import { useDebouncedValue } from "@mantine/hooks";
import { import {
AlertTriangle, AlertTriangle,
@@ -792,24 +793,19 @@ export default function BatchBoardPage() {
w={140} w={140}
styles={{ input: { borderColor: "var(--mantine-color-gray-3)" } }} styles={{ input: { borderColor: "var(--mantine-color-gray-3)" } }}
/> />
<DateInput <DatePickerInput
type="range"
size="sm" size="sm"
radius="lg" radius="lg"
placeholder="Departs from" placeholder="Departure date range"
value={departureFrom} value={[departureFrom, departureTo]}
onChange={(v) => setDepartureFrom(v ? new Date(v) : null)} onChange={([from, to]) => {
setDepartureFrom(from ? new Date(from) : null);
setDepartureTo(to ? new Date(to) : null);
}}
presets={getDateRangePresets()}
clearable clearable
w={140} w={230}
styles={{ input: { borderColor: "var(--mantine-color-gray-3)" } }}
/>
<DateInput
size="sm"
radius="lg"
placeholder="Departs to"
value={departureTo}
onChange={(v) => setDepartureTo(v ? new Date(v) : null)}
clearable
w={140}
styles={{ input: { borderColor: "var(--mantine-color-gray-3)" } }} styles={{ input: { borderColor: "var(--mantine-color-gray-3)" } }}
/> />
<Select <Select