Files
sigpro-ui/src/components/Datepicker.js

263 lines
9.6 KiB
JavaScript

// 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) })),
]);
};