-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Description
import React, { useEffect, useMemo, useState } from "react";
import { jsPDF } from "jspdf";
import autoTable from "jspdf-autotable";
import * as XLSX from "xlsx";
const DAY_LABELS = ["LUN", "MAR", "MIE", "JUE", "VIE", "SAB", "DOM"];
const HOURS = Array.from({ length: 20 }, (_, i) => 5 + i);
function startOfWeek(date) {
const d = new Date(date);
const day = (d.getDay() + 6) % 7;
const monday = new Date(d);
monday.setDate(d.getDate() - day);
monday.setHours(0, 0, 0, 0);
return monday;
}
function addDays(date, days) {
const d = new Date(date);
d.setDate(d.getDate() + days);
return d;
}
function fmtDM(d) {
const dd = String(d.getDate()).padStart(2, "0");
const mm = String(d.getMonth() + 1).padStart(2, "0");
return ${dd}/${mm}
;
}
function fmtRangeDM(weekStart) {
const a = fmtDM(weekStart);
const b = fmtDM(addDays(weekStart, 6));
return ${a}–${b}
;
}
function sameDay(a, b) {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
function parseUserDM(dm, baseDate) {
const [dd, mm] = dm.split("/").map((x) => parseInt(x, 10));
if (!dd || !mm) return null;
const d = new Date(baseDate);
d.setMonth(mm - 1, dd);
d.setHours(0, 0, 0, 0);
return isNaN(d.getTime()) ? null : d;
}
const CATEGORY_STYLES = {
CITA: {
badge: "bg-orange-500 text-white",
soft: "bg-orange-50 border-orange-200",
},
TRABAJO: {
badge: "bg-sky-400 text-white",
soft: "bg-sky-50 border-sky-200",
},
ESTUDIO: {
badge: "bg-green-500 text-white",
soft: "bg-green-50 border-green-200",
},
PERSONAL: {
badge: "bg-yellow-400 text-black",
soft: "bg-yellow-50 border-yellow-200",
},
};
const LS_KEY_FIXED = "horario-fixed-v1";
const LS_KEY_WEEK = (weekKey) => horario-week-${weekKey}
;
function weekKeyFromDate(d) {
const ws = startOfWeek(d);
return ${ws.getFullYear()}-${ws.getMonth() + 1}-${ws.getDate()}
;
}
export default function HorarioSemanal() {
const [weekStart, setWeekStart] = useState(() => startOfWeek(new Date()));
const [category, setCategory] = useState("CITA");
const [dm, setDm] = useState("");
const [hInicio, setHInicio] = useState(5);
const [hFin, setHFin] = useState(6);
const [desc, setDesc] = useState("");
const [tipo, setTipo] = useState("VARIABLE");
const [msg, setMsg] = useState("");
const [fixedEvents, setFixedEvents] = useState([]);
const [weekEvents, setWeekEvents] = useState([]);
useEffect(() => {
try {
const raw = localStorage.getItem(LS_KEY_FIXED);
if (raw) setFixedEvents(JSON.parse(raw));
} catch {}
}, []);
useEffect(() => {
try {
const key = LS_KEY_WEEK(weekKeyFromDate(weekStart));
const raw = localStorage.getItem(key);
setWeekEvents(raw ? JSON.parse(raw) : []);
} catch {
setWeekEvents([]);
}
}, [weekStart]);
useEffect(() => {
try {
localStorage.setItem(LS_KEY_FIXED, JSON.stringify(fixedEvents));
} catch {}
}, [fixedEvents]);
useEffect(() => {
try {
const key = LS_KEY_WEEK(weekKeyFromDate(weekStart));
localStorage.setItem(key, JSON.stringify(weekEvents));
} catch {}
}, [weekEvents, weekStart]);
const daysOfWeek = useMemo(() => {
return Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
}, [weekStart]);
const rangeLabel = useMemo(() => fmtRangeDM(weekStart), [weekStart]);
const events = useMemo(() => {
const all = [...fixedEvents, ...weekEvents];
return all.filter((ev) => daysOfWeek.some((d) => sameDay(d, new Date(ev.date))));
}, [fixedEvents, weekEvents, daysOfWeek]);
function addEvent() {
setMsg("");
const d = parseUserDM(dm, weekStart);
if (!d) return setMsg("❗ Formato de fecha inválido");
const inWeek = daysOfWeek.some((wd) => sameDay(wd, d));
if (!inWeek) return setMsg("
const hi = Number(hInicio);
const hf = Number(hFin);
if (hf <= hi) return setMsg("❗ Hora fin debe ser mayor a inicio");
const ev = {
id: crypto.randomUUID(),
type: tipo,
category,
date: d.toISOString(),
start: hi,
end: hf,
desc: desc.trim() || "(Sin descripción)",
};
if (tipo === "FIJO") setFixedEvents((s) => [...s, ev]);
else setWeekEvents((s) => [...s, ev]);
setDesc("");
}
function deleteEvent(id, eventType) {
if (eventType === "FIJO") setFixedEvents((s) => s.filter((e) => e.id !== id));
else setWeekEvents((s) => s.filter((e) => e.id !== id));
}
const eventsByDay = useMemo(() => {
const map = new Map();
daysOfWeek.forEach((d, i) => map.set(i, []));
events.forEach((ev) => {
const d = new Date(ev.date);
const idx = daysOfWeek.findIndex((wd) => sameDay(wd, d));
if (idx >= 0) map.get(idx).push(ev);
});
for (const [k, arr] of map.entries()) {
arr.sort((a, b) => a.start - b.start);
}
return map;
}, [events, daysOfWeek]);
function exportExcel() {
const data = events.map(ev => ({
Categoría: ev.category,
Fecha: fmtDM(new Date(ev.date)),
Inicio: ${ev.start}:00
,
Fin: ${ev.end}:00
,
Descripción: ev.desc,
Tipo: ev.type
}));
const ws = XLSX.utils.json_to_sheet(data);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Horario");
XLSX.writeFile(wb, "horario.xlsx");
}
function exportPDF() {
const doc = new jsPDF({ orientation: "landscape" });
doc.text("Horario Semanal", 14, 10);
autoTable(doc, {
head: [["Categoría", "Fecha", "Inicio", "Fin", "Descripción", "Tipo"]],
body: events.map(ev => [
ev.category,
fmtDM(new Date(ev.date)),
${ev.start}:00
,
${ev.end}:00
,
ev.desc,
ev.type
])
});
doc.save("horario.pdf");
}
return (
HORARIO SEMANAL
Categoría
{Object.keys(CATEGORY_STYLES).map((key) => (
<button key={key} onClick={() => setCategory(key)}
className={
px-2 py-1 rounded ${category === key ? CATEGORY_STYLES[key].badge : "bg-white border"}
}>{key}
))}
Fecha
<input value={dm} onChange={(e) => setDm(e.target.value)} placeholder="dd/mm" className="border rounded w-full" />
Inicio
<input type="number" value={hInicio} onChange={(e) => setHInicio(e.target.value)} min={5} max={24} className="border rounded w-full" />
Fin
<input type="number" value={hFin} onChange={(e) => setHFin(e.target.value)} min={5} max={24} className="border rounded w-full" />
Tipo
<select value={tipo} onChange={(e) => setTipo(e.target.value)} className="border rounded w-full">
Fijo
Variable
Descripción
<input value={desc} onChange={(e) => setDesc(e.target.value)} className="border rounded w-full" />
{msg &&
Agregar
<section className="grid" style={{ gridTemplateColumns: "96px repeat(7, 1fr)" }}>
{daysOfWeek.map((d, i) => (
{DAY_LABELS[i]}
{fmtDM(d)}
))}
{HOURS.map((h) => (
<React.Fragment key={h}>
{daysOfWeek.map((_, dayIdx) => (
{eventsByDay.get(dayIdx)?.filter(ev => ev.start === h).map((ev) => {
const cat = CATEGORY_STYLES[ev.category] || CATEGORY_STYLES.PERSONAL;
return (
<div key={ev.id} className={
absolute inset-0 rounded ${cat.soft}
}><div className={
${cat.badge} px-2 py-1 flex justify-between
}>{ev.category}
<button onClick={() => deleteEvent(ev.id, ev.type)} className="text-white font-bold">×
);
})}
))}
</React.Fragment>
))}
Exportar a Excel
Exportar a PDF
);
}