'use client'; /* Origin: marketing-stats (96S1), verbatim. */ import { useState, useRef, useEffect } from 'react'; import { CalendarDays, ChevronDown } from 'lucide-react'; import { MonthGrid } from './MonthGrid'; import { businessToday, formatUtcMonthLabel, isFutureBusinessMonth, utcMonthStart, utcMonthEnd, utcYearStart, } from '@/lib/dateUtils'; import { useDismiss } from '@/lib/use-dismiss'; interface DateRange { startDate: Date | null; endDate: Date | null; } interface MonthRangePickerProps { onDateRangeChange: (range: DateRange) => void; initialRange?: DateRange; className?: string; } type QuickOption = 'thisMonth' | 'lastMonth' | 'last3Months' | 'last6Months' | 'yearToDate' | 'lastYear'; // initialRange can come from a Zustand `persist` store — after rehydrating // from localStorage, Date fields arrive as plain ISO strings (JSON has no // Date type), not real Date instances. Coerce defensively before calling any // Date method on them. function toDateOrNull(value: Date | string | null | undefined): Date | null { if (!value) return null; return value instanceof Date ? value : new Date(value); } export default function MonthRangePicker({ onDateRangeChange, initialRange, className = '' }: MonthRangePickerProps) { const [isOpen, setIsOpen] = useState(false); const normalizedInitialRange = initialRange ? { startDate: toDateOrNull(initialRange.startDate), endDate: toDateOrNull(initialRange.endDate) } : { startDate: null, endDate: null }; const [range, setRange] = useState(normalizedInitialRange); // The month/year currently highlighted in each picker column, independent of // an already-applied range so browsing doesn't commit until Apply. // Read with getUTC* — the range's UTC fields are the intended calendar month // — falling back to the current GMT+8 month rather than the browser's. const [startYear, setStartYear] = useState(() => normalizedInitialRange.startDate?.getUTCFullYear() ?? businessToday().year); const [startMonth, setStartMonth] = useState(() => normalizedInitialRange.startDate?.getUTCMonth() ?? businessToday().month); const [endYear, setEndYear] = useState(() => normalizedInitialRange.endDate?.getUTCFullYear() ?? businessToday().year); const [endMonth, setEndMonth] = useState(() => normalizedInitialRange.endDate?.getUTCMonth() ?? businessToday().month); const popupRef = useRef(null); useEffect(() => { if (initialRange) { const startDate = toDateOrNull(initialRange.startDate); const endDate = toDateOrNull(initialRange.endDate); setRange({ startDate, endDate }); if (startDate) { setStartYear(startDate.getUTCFullYear()); setStartMonth(startDate.getUTCMonth()); } if (endDate) { setEndYear(endDate.getUTCFullYear()); setEndMonth(endDate.getUTCMonth()); } } }, [initialRange]); useDismiss(popupRef, isOpen, () => setIsOpen(false)); // Every preset is expressed in whole GMT+8 calendar months and built with the // UTC-anchored constructors — no date-fns arithmetic, which would operate on // the browser's local fields. Date.UTC normalizes out-of-range months, so // `month - 5` rolling into the previous year needs no special case. const handleQuickSelect = (option: QuickOption) => { const { year, month } = businessToday(); let start: Date; let end: Date; switch (option) { case 'thisMonth': start = utcMonthStart(year, month); end = utcMonthEnd(year, month); break; case 'lastMonth': start = utcMonthStart(year, month - 1); end = utcMonthEnd(year, month - 1); break; case 'last3Months': start = utcMonthStart(year, month - 2); end = utcMonthEnd(year, month); break; case 'last6Months': start = utcMonthStart(year, month - 5); end = utcMonthEnd(year, month); break; case 'yearToDate': start = utcYearStart(year); end = utcMonthEnd(year, month); break; case 'lastYear': start = utcYearStart(year - 1); end = utcMonthEnd(year - 1, 11); break; default: return; } setRange({ startDate: start, endDate: end }); setStartYear(start.getUTCFullYear()); setStartMonth(start.getUTCMonth()); setEndYear(end.getUTCFullYear()); setEndMonth(end.getUTCMonth()); }; const handlePickStart = (year: number, month: number) => { setStartYear(year); setStartMonth(month); setRange(prev => ({ ...prev, startDate: utcMonthStart(year, month) })); }; const handlePickEnd = (year: number, month: number) => { setEndYear(year); setEndMonth(month); setRange(prev => ({ ...prev, endDate: utcMonthEnd(year, month) })); }; const handleApply = () => { if (!range.startDate || !range.endDate) return; onDateRangeChange(range); setIsOpen(false); }; const handleClear = () => { setRange({ startDate: null, endDate: null }); }; const formatDisplay = () => { if (range.startDate && range.endDate) { return `${formatUtcMonthLabel(range.startDate)} - ${formatUtcMonthLabel(range.endDate)}`; } return 'Select month range'; }; const quickOptions: { key: QuickOption; label: string }[] = [ { key: 'thisMonth', label: 'This Month' }, { key: 'lastMonth', label: 'Last Month' }, { key: 'last3Months', label: 'Last 3 Months' }, { key: 'last6Months', label: 'Last 6 Months' }, { key: 'yearToDate', label: 'Year to Date' }, { key: 'lastYear', label: 'Last Year' }, ]; return (
{isOpen && (

{range.startDate && range.endDate ? `${formatUtcMonthLabel(range.startDate)} – ${formatUtcMonthLabel(range.endDate)}` : 'Select a month range'}

Quick Selection

{quickOptions.map(option => ( ))}

From

handlePickStart(y, startMonth)} onPick={handlePickStart} isMonthDisabled={isFutureBusinessMonth} />

To

handlePickEnd(y, endMonth)} onPick={handlePickEnd} isMonthDisabled={isFutureBusinessMonth} />
)}
); }