// components/Datepicker.js // import { $, Tag, If } from "../sigpro.js"; import { val, ui, getIcon } from "../core/utils.js"; import { Input } from "./Input.js"; /** * Datepicker component * * daisyUI classes used: * - input, input-bordered, input-primary * - btn, btn-ghost, btn-xs, btn-circle * - bg-base-100, border, border-base-300, shadow-2xl, rounded-box * - absolute, left-0, mt-2, p-4, w-80, z-100, z-90 * - grid, grid-cols-7, gap-1, text-center * - ring, ring-primary, ring-inset, font-black * - range, range-xs * - tooltip, tooltip-top, tooltip-bottom */ export const Datepicker = (props) => { const { class: className, value, range, label, placeholder, hour = false, ...rest } = props; const isOpen = $(false); const internalDate = $(new Date()); const hoverDate = $(null); const startHour = $(0); const endHour = $(0); const isRangeMode = () => val(range) === true; const now = new Date(); const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; const formatDate = (d) => { const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; }; const selectDate = (date) => { const dateStr = formatDate(date); const current = val(value); if (isRangeMode()) { if (!current?.start || (current.start && current.end)) { if (typeof value === "function") { value({ start: dateStr, end: null, ...(hour && { startHour: startHour() }), }); } } else { const start = current.start; if (typeof value === "function") { const newValue = dateStr < start ? { start: dateStr, end: start } : { start, end: dateStr }; if (hour) { newValue.startHour = current.startHour || startHour(); newValue.endHour = current.endHour || endHour(); } value(newValue); } isOpen(false); } } else { if (typeof value === "function") { value(hour ? `${dateStr}T${String(startHour()).padStart(2, "0")}:00:00` : dateStr); } isOpen(false); } }; // FIX CRÍTICO: Señal manual y Watch para displayValue const displayValue = $(""); Watch(() => { const v = val(value); if (!v) { displayValue(""); return; } let text = ""; if (typeof v === "string") { text = (hour && v.includes("T")) ? v.replace("T", " ") : v; } else if (v.start && v.end) { const startStr = hour && v.startHour !== undefined ? `${v.start} ${String(v.startHour).padStart(2, "0")}:00` : v.start; const endStr = hour && v.endHour !== undefined ? `${v.end} ${String(v.endHour).padStart(2, "0")}:00` : v.end; text = `${startStr} - ${endStr}`; } else if (v.start) { const startStr = hour && v.startHour !== undefined ? `${v.start} ${String(v.startHour).padStart(2, "0")}:00` : v.start; text = `${startStr}...`; } displayValue(text); }); const move = (m) => { const d = internalDate(); internalDate(new Date(d.getFullYear(), d.getMonth() + m, 1)); }; const moveYear = (y) => { const d = internalDate(); internalDate(new Date(d.getFullYear() + y, d.getMonth(), 1)); }; const HourSlider = ({ value: hVal, onChange }) => { return Tag("div", { class: "flex-1" }, [ Tag("div", { class: "flex gap-2 items-center" }, [ Tag("input", { type: "range", min: 0, max: 23, value: hVal, // Sincronizado con hVal class: "range range-xs flex-1", oninput: (e) => { const newHour = parseInt(e.target.value); onChange(newHour); }, }), Tag("span", { class: "text-sm font-mono min-w-[48px] text-center" }, () => String(val(hVal)).padStart(2, "0") + ":00" ), ]), ]); }; return Tag("div", { class: ui('relative w-full', className) }, [ Input({ label, placeholder: placeholder || (isRangeMode() ? "Seleccionar rango..." : "Seleccionar fecha..."), value: displayValue, // Ahora es una señal que actualizamos manualmente readonly: true, icon: getIcon("icon-[lucide--calendar]"), onclick: (e) => { e.stopPropagation(); isOpen(!isOpen()); }, ...rest, }), If(isOpen, () => Tag( "div", { class: "absolute left-0 mt-2 p-4 bg-base-100 border border-base-300 shadow-2xl rounded-box z-[100] w-80 select-none", onclick: (e) => e.stopPropagation(), }, [ Tag("div", { class: "flex justify-between items-center mb-4 gap-1" }, [ Tag("div", { class: "flex gap-0.5" }, [ Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(-1) }, getIcon("icon-[lucide--chevrons-left]") ), Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(-1) }, getIcon("icon-[lucide--chevron-left]") ), ]), Tag("span", { class: "font-bold uppercase flex-1 text-center" }, [ () => internalDate().toLocaleString("es-ES", { month: "short", year: "numeric" }), ]), Tag("div", { class: "flex gap-0.5" }, [ Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => move(1) }, getIcon("icon-[lucide--chevron-right]") ), Tag("button", { type: "button", class: "btn btn-ghost btn-xs px-1", onclick: () => moveYear(1) }, getIcon("icon-[lucide--chevrons-right]") ), ]), ]), Tag("div", { class: "grid grid-cols-7 gap-1", onmouseleave: () => hoverDate(null) }, [ ...["L", "M", "X", "J", "V", "S", "D"].map((d) => Tag("div", { class: "text-[10px] opacity-40 font-bold text-center" }, d)), () => { const d = internalDate(); const year = d.getFullYear(); const month = d.getMonth(); const firstDay = new Date(year, month, 1).getDay(); const offset = firstDay === 0 ? 6 : firstDay - 1; const daysInMonth = new Date(year, month + 1, 0).getDate(); const nodes = []; for (let i = 0; i < offset; i++) nodes.push(Tag("div")); for (let i = 1; i <= daysInMonth; i++) { const date = new Date(year, month, i); const dStr = formatDate(date); nodes.push( Tag( "button", { type: "button", class: () => { const v = val(value); const h = hoverDate(); const isStart = typeof v === "string" ? v.split("T")[0] === dStr : v?.start === dStr; const isEnd = v?.end === dStr; let inRange = false; if (isRangeMode() && v?.start) { const start = v.start; if (!v.end && h) { inRange = (dStr > start && dStr <= h) || (dStr < start && dStr >= h); } else if (v.end) { inRange = dStr > start && dStr < v.end; } } const base = "btn btn-xs p-0 aspect-square min-h-0 h-auto font-normal relative"; const state = isStart || isEnd ? "btn-primary z-10" : inRange ? "bg-primary/20 border-none rounded-none" : "btn-ghost"; const today = dStr === todayStr ? "ring-1 ring-primary ring-inset font-black text-primary" : ""; return `${base} ${state} ${today}`; }, onmouseenter: () => { if (isRangeMode()) hoverDate(dStr); }, onclick: () => selectDate(date), }, [i.toString()], ), ); } return nodes; }, ]), hour ? Tag("div", { class: "mt-3 pt-2 border-t border-base-300" }, [ isRangeMode() ? Tag("div", { class: "flex gap-4" }, [ HourSlider({ value: startHour, onChange: (newHour) => { startHour(newHour); const currentVal = val(value); if (currentVal?.start) value({ ...currentVal, startHour: newHour }); }, }), HourSlider({ value: endHour, onChange: (newHour) => { endHour(newHour); const currentVal = val(value); if (currentVal?.end) value({ ...currentVal, endHour: newHour }); }, }), ]) : HourSlider({ value: startHour, onChange: (newHour) => { startHour(newHour); const currentVal = val(value); if (currentVal && typeof currentVal === "string") { value(currentVal.split("T")[0] + "T" + String(newHour).padStart(2, "0") + ":00:00"); } }, }), ]) : null, ], ), ), If(isOpen, () => Tag("div", { class: "fixed inset-0 z-[90]", onclick: () => isOpen(false) })), ]); };