"use client"; import * as React from "react"; import { Calendar as CalendarIcon, X } from "lucide-react"; import { cn } from "@/lib/utils"; import { Calendar } from "@/components/ui/calendar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { isValidIsoDateYmd } from "@/lib/frontend/search/dateRange"; import { getSearchDateRangeValidation } from "@/lib/frontend/search/searchDateValidation"; import { buildDatePresets } from "@/lib/frontend/search/datePresets"; function pad2(n) { return String(n).padStart(2, "0"); } function dateToIsoYmd(date) { if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null; const y = date.getFullYear(); const m = pad2(date.getMonth() + 1); const d = pad2(date.getDate()); return `${y}-${m}-${d}`; } function isoYmdToDate(iso) { if (typeof iso !== "string") return null; if (!isValidIsoDateYmd(iso)) return null; const [y, m, d] = iso.split("-").map((x) => Number(x)); return new Date(y, m - 1, d); } const deDateFormatter = new Intl.DateTimeFormat("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", }); function formatIsoToDe(iso) { const d = isoYmdToDate(iso); return d ? deDateFormatter.format(d) : ""; } function getSummaryLabel(from, to) { if (from && to) return `${formatIsoToDe(from)} – ${formatIsoToDe(to)}`; if (from) return `ab ${formatIsoToDe(from)}`; if (to) return `bis ${formatIsoToDe(to)}`; return "Zeitraum auswählen"; } export default function SearchDateRangePicker({ from, to, onDateRangeChange, isSubmitting, className, }) { const disabled = Boolean(isSubmitting); const [open, setOpen] = React.useState(false); const [activeField, setActiveField] = React.useState("from"); // "from" | "to" const fromRef = React.useRef(null); const toRef = React.useRef(null); const fromDate = isoYmdToDate(from); const toDate = isoYmdToDate(to); const validation = getSearchDateRangeValidation(from ?? null, to ?? null); const isRangeInvalid = validation?.code === "VALIDATION_SEARCH_RANGE"; const now = React.useMemo(() => new Date(), []); const presets = React.useMemo(() => buildDatePresets({ now }), [now]); // Split into two rows (4 + 3): // Row 1: short ranges (today/yesterday/last 7/last 30) // Row 2: larger ranges (this month/last month/this year) const presetsRow1 = presets.slice(0, 4); const presetsRow2 = presets.slice(4); const [month, setMonth] = React.useState(() => { return fromDate || toDate || new Date(); }); React.useEffect(() => { if (!open) return; if (from && !to) { setActiveField("to"); requestAnimationFrame(() => toRef.current?.focus?.()); } else { setActiveField("from"); requestAnimationFrame(() => fromRef.current?.focus?.()); } setMonth(fromDate || toDate || new Date()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); function safeOnDateRangeChange(next) { if (typeof onDateRangeChange !== "function") return; const hasFrom = Object.prototype.hasOwnProperty.call(next || {}, "from"); const hasTo = Object.prototype.hasOwnProperty.call(next || {}, "to"); onDateRangeChange({ from: hasFrom ? next.from : from ?? null, to: hasTo ? next.to : to ?? null, }); } let calendarSelected = undefined; let invalidInterval = null; if (fromDate && toDate) { if (isRangeInvalid) { const min = fromDate < toDate ? fromDate : toDate; const max = fromDate < toDate ? toDate : fromDate; calendarSelected = { from: min, to: max }; invalidInterval = { from: min, to: max }; } else { calendarSelected = { from: fromDate, to: toDate }; } } else if (fromDate) { calendarSelected = { from: fromDate, to: undefined }; } else if (toDate) { calendarSelected = { from: toDate, to: toDate }; } const calendarModifiers = isRangeInvalid && invalidInterval ? { invalid_range: invalidInterval, invalid_range_edge: [fromDate, toDate].filter(Boolean), } : undefined; const calendarModifiersClassNames = isRangeInvalid && invalidInterval ? { invalid_range: "bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive", invalid_range_edge: "!bg-destructive !text-white hover:!bg-destructive/90", } : undefined; function focusActiveField(nextField) { if (nextField === "from") { requestAnimationFrame(() => fromRef.current?.focus?.()); } else { requestAnimationFrame(() => toRef.current?.focus?.()); } } function handlePickDay(...args) { if (disabled) return; let day = args[0]; let modifiers = args[1]; if (!(day instanceof Date) && args[1] instanceof Date) { day = args[1]; modifiers = args[2]; } if (!(day instanceof Date) || Number.isNaN(day.getTime())) return; if (modifiers?.disabled) return; const iso = dateToIsoYmd(day); if (!iso) return; if (activeField === "from") { safeOnDateRangeChange({ from: iso }); setActiveField("to"); focusActiveField("to"); return; } safeOnDateRangeChange({ to: iso }); setActiveField("to"); focusActiveField("to"); } function handleClearFrom(e) { e?.preventDefault?.(); e?.stopPropagation?.(); if (disabled) return; safeOnDateRangeChange({ from: null }); setActiveField("from"); focusActiveField("from"); } function handleClearTo(e) { e?.preventDefault?.(); e?.stopPropagation?.(); if (disabled) return; safeOnDateRangeChange({ to: null }); setActiveField("to"); focusActiveField("to"); } function handleReset() { if (disabled) return; safeOnDateRangeChange({ from: null, to: null }); setActiveField("from"); setMonth(new Date()); focusActiveField("from"); } function applyPreset(preset) { if (disabled) return; if (!preset?.from || !preset?.to) return; safeOnDateRangeChange({ from: preset.from, to: preset.to }); const nextMonth = isoYmdToDate(preset.from) || isoYmdToDate(preset.to) || new Date(); setMonth(nextMonth); setActiveField("to"); focusActiveField("to"); } const summary = getSummaryLabel(from, to); const activeInputClass = "border-blue-600 bg-blue-50 dark:border-blue-900 dark:bg-blue-950"; const calendarKey = `${from || ""}|${to || ""}|${ isRangeInvalid ? "inv" : "ok" }`; return (
Tipp: Für einen einzelnen Tag setzen Sie Von und{" "} Bis auf dasselbe Datum.