import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { CalendarDays, ChevronLeft, ChevronRight } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import { resolveUsageRange } from "@/lib/usageRange"; import { getLocaleFromLanguage } from "./format"; import type { UsageRangeSelection } from "@/types/usage"; type DraftField = "start" | "end"; interface UsageDateRangePickerProps { selection: UsageRangeSelection; onApply: (selection: UsageRangeSelection) => void; triggerLabel: string; } /* ── helpers ── */ function startOfDay(d: Date): Date { return new Date(d.getFullYear(), d.getMonth(), d.getDate()); } function isSameDay(a: Date, b: Date): boolean { return ( a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate() ); } function toTs(d: Date): number { return Math.floor(d.getTime() / 1000); } function fromTs(ts: number): Date { return new Date(ts * 1000); } function fmtDate(ts: number): string { const d = fromTs(ts); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } function fmtTime(ts: number): string { const d = fromTs(ts); return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; } function parseDateInput(ts: number, value: string): number { const [y, m, d] = value.split("-").map(Number); if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) return ts; const base = fromTs(ts); return toTs(new Date(y, m - 1, d, base.getHours(), base.getMinutes())); } function parseTimeInput(ts: number, value: string): number { const [h, min] = value.split(":").map(Number); if (!Number.isFinite(h) || !Number.isFinite(min)) return ts; const base = fromTs(ts); return toTs( new Date(base.getFullYear(), base.getMonth(), base.getDate(), h, min), ); } function setDateKeepTime(ts: number, day: Date): number { const base = fromTs(ts); return toTs( new Date( day.getFullYear(), day.getMonth(), day.getDate(), base.getHours(), base.getMinutes(), ), ); } function getCalendarDays(month: Date): Date[] { const first = new Date(month.getFullYear(), month.getMonth(), 1); const gridStart = new Date(first); gridStart.setDate(first.getDate() - first.getDay()); return Array.from({ length: 42 }, (_, i) => { const d = new Date(gridStart); d.setDate(gridStart.getDate() + i); return d; }); } /* ── component ── */ export function UsageDateRangePicker({ selection, onApply, triggerLabel, }: UsageDateRangePickerProps) { const { t, i18n } = useTranslation(); const [open, setOpen] = useState(false); const [activeField, setActiveField] = useState("start"); const resolvedRange = useMemo( () => resolveUsageRange(selection), [selection], ); const [draftStart, setDraftStart] = useState(resolvedRange.startDate); const [draftEnd, setDraftEnd] = useState(resolvedRange.endDate); const [displayMonth, setDisplayMonth] = useState( () => new Date( fromTs(resolvedRange.startDate).getFullYear(), fromTs(resolvedRange.startDate).getMonth(), 1, ), ); const [error, setError] = useState(null); const language = i18n.resolvedLanguage || i18n.language || "en"; const locale = getLocaleFromLanguage(language); // Reset draft when popover opens useEffect(() => { if (!open) return; const r = resolveUsageRange(selection); setDraftStart(r.startDate); setDraftEnd(r.endDate); setDisplayMonth( new Date( fromTs(r.startDate).getFullYear(), fromTs(r.startDate).getMonth(), 1, ), ); setActiveField("start"); setError(null); }, [open, selection]); const calendarDays = useMemo( () => getCalendarDays(displayMonth), [displayMonth], ); const weekdayLabels = useMemo( () => Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(locale, { weekday: "narrow" }).format( new Date(2024, 0, 7 + i), ), ), [locale], ); const startDay = fromTs(draftStart); const endDay = fromTs(draftEnd); const today = new Date(); /* Pick a date from the calendar */ const handleDatePick = (day: Date) => { setError(null); const nextTs = setDateKeepTime( activeField === "start" ? draftStart : draftEnd, day, ); if (activeField === "start") { setDraftStart(nextTs); // Auto-swap if start > end if (nextTs > draftEnd) { setDraftEnd(nextTs); } // Auto-advance to end field setActiveField("end"); } else { // If picked end < start, treat as new start and auto-advance if (nextTs < draftStart) { setDraftStart(nextTs); setActiveField("end"); } else { setDraftEnd(nextTs); } } // Navigate calendar if the day is outside the displayed month if ( day.getMonth() !== displayMonth.getMonth() || day.getFullYear() !== displayMonth.getFullYear() ) { setDisplayMonth(new Date(day.getFullYear(), day.getMonth(), 1)); } }; const handleApply = () => { setError(null); if (draftStart > draftEnd) { setError(t("usage.invalidTimeRangeOrder", "开始时间不能晚于结束时间")); return; } onApply({ preset: "custom", customStartDate: draftStart, customEndDate: draftEnd, }); setOpen(false); }; const goToToday = () => { setDisplayMonth(new Date(today.getFullYear(), today.getMonth(), 1)); }; /* ── Field card (start / end) ── */ const renderField = (field: DraftField) => { const isActive = activeField === field; const ts = field === "start" ? draftStart : draftEnd; const setTs = field === "start" ? setDraftStart : setDraftEnd; const label = field === "start" ? t("usage.startTime", "开始时间") : t("usage.endTime", "结束时间"); return (
setActiveField(field)} >
{label}
{ const next = parseDateInput(ts, e.target.value); setTs(next); const d = fromTs(next); setDisplayMonth(new Date(d.getFullYear(), d.getMonth(), 1)); setError(null); }} onFocus={() => setActiveField(field)} /> { setTs(parseTimeInput(ts, e.target.value)); setError(null); }} onFocus={() => setActiveField(field)} />
); }; return (
{/* Left: date fields */}

{t("usage.customRangeHint", "支持日期与时间,最长 30 天")}

{renderField("start")} {renderField("end")} {error &&

{error}

}
{/* Right: calendar */}
{/* Month navigation */}
{/* Weekday headers */}
{weekdayLabels.map((label, i) => (
{label}
))}
{/* Day grid */}
{calendarDays.map((day) => { const isCurrentMonth = day.getMonth() === displayMonth.getMonth(); const isToday = isSameDay(day, today); const isStart = isSameDay(day, startDay); const isEnd = isSameDay(day, endDay); const dayStart = startOfDay(day); const inRange = dayStart >= startOfDay(startDay) && dayStart <= startOfDay(endDay); const isEndpoint = isStart || isEnd; return ( ); })}
); }