import { useMemo, useState } from "react"; import { daysInMonth, gregorianToJalali, isoToJalaliStr, jalaliStrToIso, jalaliToGregorian, MONTHS, toFa, WEEKDAYS, } from "../lib/jalali"; import { faNum } from "../lib/formatters"; import { cn } from "../lib/utils"; import { useClickOutside } from "./useClickOutside"; export interface JalaliDatePickerProps { value: string | null; // ISO date (با time اختیاری) onChange: (iso: string | null) => void; placeholder?: string; className?: string; withTime?: boolean; // نمایش ساعت:دقیقه هم (پیشنهاد/پذیرش/رسید) } function todayJalali(): [number, number, number] { const now = new Date(); return gregorianToJalali(now.getFullYear(), now.getMonth() + 1, now.getDate()); } function isoToJalali(iso: string | null): [number, number, number] | null { if (!iso) return null; const d = new Date(iso); if (Number.isNaN(d.getTime())) return null; return gregorianToJalali(d.getFullYear(), d.getMonth() + 1, d.getDate()); } // ساخت ISO با زمان محلی (تاریخ شمسی + ساعت:دقیقه) function buildIso(jy: number, jm: number, jd: number, h: number, m: number): string { const [gy, gm, gd] = jalaliToGregorian(jy, jm, jd); return new Date(gy, gm - 1, gd, h, m).toISOString(); } function timeOf(iso: string | null): { h: number; m: number } { if (!iso) return { h: 0, m: 0 }; const d = new Date(iso); if (Number.isNaN(d.getTime())) return { h: 0, m: 0 }; return { h: d.getHours(), m: d.getMinutes() }; } // تقویم شمسی — پورت Jalali.buildPicker به React (با پشتیبانی ساعت:دقیقه) export function JalaliDatePicker({ value, onChange, placeholder, className, withTime = false, }: JalaliDatePickerProps) { const [open, setOpen] = useState(false); const [curJ, setCurJ] = useState<[number, number, number]>(() => isoToJalali(value) ?? todayJalali()); const [text, setText] = useState(() => { if (!value) return ""; const j = isoToJalaliStr(value); if (!withTime) return j; const d = new Date(value); if (Number.isNaN(d.getTime())) return j; const hh = String(d.getHours()).padStart(2, "0"); const mm = String(d.getMinutes()).padStart(2, "0"); return `${j} ${faNum(hh + ":" + mm)}`; }); const ref = useClickOutside(() => setOpen(false)); // تاریخ انتخاب‌شده به صورت محلی (نه UTC) برای مقایسه با سلول‌های تقویم function selectedLocalDate(): string | null { if (!value) return null; const d = new Date(value); if (Number.isNaN(d.getTime())) return null; return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } const selectedIso = selectedLocalDate(); const time = timeOf(value); const grid = useMemo(() => { const [jy, jm] = curJ; const firstG = jalaliToGregorian(jy, jm, 1); const firstWd = (new Date(firstG[0], firstG[1] - 1, firstG[2]).getDay() + 1) % 7; // شنبه = 0 const dim = daysInMonth(jy, jm); const today = new Date(); const tIso = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`; const cells: { d: number; iso: string; isToday: boolean }[] = []; for (let i = 0; i < firstWd; i++) cells.push({ d: 0, iso: "", isToday: false }); for (let d = 1; d <= dim; d++) { const [gy, gm, gd] = jalaliToGregorian(jy, jm, d); const iso = `${gy}-${String(gm).padStart(2, "0")}-${String(gd).padStart(2, "0")}`; cells.push({ d, iso, isToday: iso === tIso }); } return cells; }, [curJ]); const nav = (dir: string) => { setCurJ(([jy, jm]) => { let ny = jy; let nm = jm; if (dir === "m-1") nm = nm === 12 ? 1 : nm + 1; if (dir === "m+1") nm = nm === 1 ? 12 : nm - 1; if (dir === "y-1") ny += 1; if (dir === "y+1") ny -= 1; if (nm < 1) { nm = 12; ny -= 1; } if (nm > 12) { nm = 1; ny += 1; } return [ny, nm, 1]; }); }; const selectDay = (jy: number, jm: number, jd: number) => { const isoWithTime = buildIso(jy, jm, jd, time.h, time.m); onChange(isoWithTime); setText(`${toFa(jy)}/${toFa(String(jm).padStart(2, "0"))}/${toFa(String(jd).padStart(2, "0"))}${withTime ? " " + faNum(String(time.h).padStart(2, "0") + ":" + String(time.m).padStart(2, "0")) : ""}`); setOpen(false); }; const setTimePart = (part: "h" | "m", raw: string) => { const n = parseInt(raw.replace(/[۰-۹]/g, (d) => String("۰۱۲۳۴۵۶۷۸۹".indexOf(d))), 10); if (Number.isNaN(n)) return; const h = part === "h" ? Math.min(23, Math.max(0, n)) : time.h; const m = part === "m" ? Math.min(59, Math.max(0, n)) : time.m; const [jy, jm, jd] = curJ; onChange(buildIso(jy, jm, jd, h, m)); // به‌روزرسانی نمایش const j = value ? isoToJalaliStr(value) : isoToJalaliStr(buildIso(jy, jm, jd, h, m)); setText(`${j} ${faNum(String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0"))}`); }; const pickToday = () => { const now = new Date(); const [jy, jm, jd] = todayJalali(); // بدون withTime فقط تاریخ امروز ثبت می‌شود — ساعت همیشه نیمه‌شب const isoWithTime = withTime ? buildIso(jy, jm, jd, now.getHours(), now.getMinutes()) : buildIso(jy, jm, jd, 0, 0); onChange(isoWithTime); setText( withTime ? `${toFa(jy)}/${toFa(String(jm).padStart(2, "0"))}/${toFa(String(jd).padStart(2, "0"))} ${faNum(String(now.getHours()).padStart(2, "0") + ":" + String(now.getMinutes()).padStart(2, "0"))}` : `${toFa(jy)}/${toFa(String(jm).padStart(2, "0"))}/${toFa(String(jd).padStart(2, "0"))}`, ); setOpen(false); }; const clear = () => { onChange(null); setText(""); setOpen(false); }; const onInputChange = (v: string) => { setText(v); const iso = jalaliStrToIso(v); if (iso) { onChange(iso); const j = isoToJalali(iso); if (j) setCurJ([j[0], j[1], j[2]]); } }; return (
setOpen(true)} onClick={() => setOpen(true)} onChange={(e) => onInputChange(e.target.value)} /> {open && (
{MONTHS[curJ[1] - 1]} {toFa(curJ[0])}
{WEEKDAYS.map((w) => (
{w}
))} {grid.map((cell, i) => cell.d === 0 ? (
) : ( ), )}
{withTime && (
ساعت setTimePart("h", e.target.value)} />
:
دقیقه setTimePart("m", e.target.value)} />
)}
)}
); }