diff --git a/src/components/UI/DateRangePicker.jsx b/src/components/UI/DateRangePicker.jsx new file mode 100644 index 0000000..cc70f4f --- /dev/null +++ b/src/components/UI/DateRangePicker.jsx @@ -0,0 +1,180 @@ +import React, { useState, useRef, useEffect } from "react"; + +const MONTH_NAMES = [ + "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", + "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь", +]; +const WEEKDAY_SHORT = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]; + +function getDaysInMonth(year, month) { return new Date(year, month + 1, 0).getDate(); } +function getFirstDayOfWeek(year, month) { const d = new Date(year, month, 1).getDay(); return d === 0 ? 6 : d - 1; } +function formatDateISO(date) { + if (!date) return ""; + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`; +} +function formatDateDisplay(date) { + if (!date) return ""; + return `${String(date.getDate()).padStart(2, "0")}.${String(date.getMonth() + 1).padStart(2, "0")}.${date.getFullYear()}`; +} +function parseDateFromISO(str) { + if (!str) return null; + const [y, m, d] = str.split("-").map(Number); + if (!y || !m || !d) return null; + return new Date(y, m - 1, d); +} + +/** + * DateRangePicker — одно поле «Период» без лейбла. + * При клике открывает календарь: первый клик = «с», второй = «по». + * Показывает «С даты — По дату» или «Период» если пусто. + */ +export const DateRangePicker = ({ valueFrom = "", valueTo = "", onChange, className = "" }) => { + const [isOpen, setIsOpen] = useState(false); + const [viewYear, setViewYear] = useState(() => { + const d = parseDateFromISO(valueFrom) || new Date(); + return d.getFullYear(); + }); + const [viewMonth, setViewMonth] = useState(() => { + const d = parseDateFromISO(valueFrom) || new Date(); + return d.getMonth(); + }); + const [selecting, setSelecting] = useState("from"); // "from" | "to" + const containerRef = useRef(null); + + useEffect(() => { + if (!isOpen) return; + const handle = (e) => { + if (containerRef.current && !containerRef.current.contains(e.target)) setIsOpen(false); + }; + document.addEventListener("pointerdown", handle); + return () => document.removeEventListener("pointerdown", handle); + }, [isOpen]); + + useEffect(() => { + const handle = (e) => { if (e.key === "Escape" && isOpen) setIsOpen(false); }; + document.addEventListener("keydown", handle); + return () => document.removeEventListener("keydown", handle); + }, [isOpen]); + + const fromDate = parseDateFromISO(valueFrom); + const toDate = parseDateFromISO(valueTo); + + const handleDayClick = (day) => { + const d = new Date(viewYear, viewMonth, day); + if (selecting === "from") { + onChange(formatDateISO(d), valueTo); + setSelecting("to"); + } else { + // Если выбрали дату раньше «с» — поменяем местами + const from = parseDateFromISO(valueFrom); + if (from && d < from) { + onChange(formatDateISO(d), valueFrom); + } else { + onChange(valueFrom, formatDateISO(d)); + } + setIsOpen(false); + setSelecting("from"); + } + }; + + const handleClear = (e) => { + e.stopPropagation(); + onChange("", ""); + setSelecting("from"); + }; + + const prevMonth = () => { + if (viewMonth === 0) { setViewMonth(11); setViewYear((y) => y - 1); } + else setViewMonth((m) => m - 1); + }; + const nextMonth = () => { + if (viewMonth === 11) { setViewMonth(0); setViewYear((y) => y + 1); } + else setViewMonth((m) => m + 1); + }; + + const daysInMonth = getDaysInMonth(viewYear, viewMonth); + const firstDay = getFirstDayOfWeek(viewYear, viewMonth); + const today = new Date(); + const isToday = (day) => day === today.getDate() && viewMonth === today.getMonth() && viewYear === today.getFullYear(); + const isInRange = (day) => { + const d = new Date(viewYear, viewMonth, day); + if (fromDate && toDate) return d >= fromDate && d <= toDate; + return false; + }; + const isSelected = (day) => { + const d = new Date(viewYear, viewMonth, day); + return (fromDate && d.getTime() === fromDate.getTime()) || (toDate && d.getTime() === toDate.getTime()); + }; + + const cells = []; + for (let i = 0; i < firstDay; i++) cells.push(null); + for (let d = 1; d <= daysInMonth; d++) cells.push(d); + + const displayText = fromDate + ? `${formatDateDisplay(fromDate)} — ${toDate ? formatDateDisplay(toDate) : "…"}` + : "Период"; + + return ( +