import React, { useState, useMemo } from “react”; const TURNOS = { mañana: { label: “Mañana”, color: “#F59E0B” }, tarde: { label: “Tarde”, color: “#F97316” }, noche: { label: “Noche”, color: “#7C3AED” }, descanso: { label: “Descanso”, color: “#10B981” }, vacaciones: { label: “Vacaciones”, color: “#3B82F6” } }; const DIAS_CORTOS = [“Dom”, “Lun”, “Mar”, “Mié”, “Jue”, “Vie”, “Sáb”]; const DIAS_LARGOS = [“Domingo”, “Lunes”, “Martes”, “Miércoles”, “Jueves”, “Viernes”, “Sábado”]; const MESES = [“Enero”, “Febrero”, “Marzo”, “Abril”, “Mayo”, “Junio”, “Julio”, “Agosto”, “Septiembre”, “Octubre”, “Noviembre”, “Diciembre”]; function nuevoId() { return Math.random().toString(36).slice(2, 9); } function fechaToStr(d) { return d.getFullYear() + “-” + String(d.getMonth() + 1).padStart(2, “0”) + “-” + String(d.getDate()).padStart(2, “0”); } function strToFecha(s) { const p = s.split(”-”).map(Number); return new Date(p[0], p[1] - 1, p[2]); } function diasEntre(a, b) { return Math.round((strToFecha(b) - strToFecha(a)) / 86400000); } function turnoDelCiclo(ciclo, fecha, inicio) { if (!ciclo || !inicio) return null; const d = diasEntre(inicio, fecha); if (d < 0) return null; return ciclo.dias[d % (ciclo.semanas * 7)] || null; } const ESTILO_INPUT = “block w-full border border-gray-300 rounded-lg px-3 py-2 text-base sm:text-sm bg-white”; function Pildora({ turno }) { const t = TURNOS[turno]; if (!t) return null; return ( {t.label} ); } function SelectorTurno({ value, onChange }) { return ( ); } function Campo({ label, children }) { return (

{label}

{children}
); } function ModalDialog({ title, onClose, wide, children }) { return (
{title}
{children}
); } function BotonPrimario({ children, onClick, disabled, full, sm }) { const tam = sm ? “px-3 py-2 text-sm” : “px-4 py-2.5 text-sm”; const ancho = full ? “ w-full” : “”; return ( ); } function BotonSecundario({ children, onClick, full, sm }) { const tam = sm ? “px-3 py-2 text-sm” : “px-4 py-2.5 text-sm”; const ancho = full ? “ w-full” : “”; return ( ); } function BotonPeligro({ children, onClick, sm }) { const tam = sm ? “px-3 py-2 text-sm” : “px-4 py-2.5 text-sm”; return ( ); } export default function App() { const [tab, setTab] = useState(“calendario”); const [colaboradores, setColaboradores] = useState([]); const [equipos, setEquipos] = useState([]); const [ciclos, setCiclos] = useState([]); const [minimos, setMinimos] = useState([]); const [asignaciones, setAsignaciones] = useState({}); const [aplicaciones, setAplicaciones] = useState([]); function obtenerTurno(colId, fecha) { const clave = colId + “_” + fecha; if (asignaciones[clave]) return asignaciones[clave]; const apsCol = aplicaciones.filter((a) => a.colId === colId && a.inicio <= fecha); apsCol.sort((a, b) => b.inicio.localeCompare(a.inicio)); if (apsCol.length === 0) return null; const ap = apsCol[0]; const ciclo = ciclos.find((c) => c.id === ap.cicloId); return turnoDelCiclo(ciclo, fecha, ap.inicio); } const TABS = [ [“calendario”, “📅”, “Calendario”], [“colaboradores”, “👥”, “Personas”], [“equipos”, “🏷️”, “Equipos”], [“ciclos”, “🔄”, “Ciclos”], [“minimos”, “⚠️”, “Mínimos”] ]; return (

📅 Gestor de Guardias

{TABS.map(([id, icon, label]) => { const activo = tab === id; const cls = “flex items-center gap-1.5 px-3 sm:px-4 py-3 text-sm font-semibold whitespace-nowrap border-b-2 transition-colors “ + (activo ? “border-indigo-600 text-indigo-700” : “border-transparent text-gray-500 hover:text-gray-700”); return ( ); })}
{tab === “calendario” && ( )} {tab === “colaboradores” && ( )} {tab === “equipos” && ( )} {tab === “ciclos” && } {tab === “minimos” && }
); } function TabCalendario({ colaboradores, equipos, minimos, asignaciones, setAsignaciones, obtenerTurno }) { const hoy = new Date(); const [año, setAño] = useState(hoy.getFullYear()); const [mes, setMes] = useState(hoy.getMonth()); const [diaAbierto, setDiaAbierto] = useState(null); function mesAnterior() { if (mes === 0) { setAño(año - 1); setMes(11); } else setMes(mes - 1); } function mesSiguiente() { if (mes === 11) { setAño(año + 1); setMes(0); } else setMes(mes + 1); } const diasDelMes = new Date(año, mes + 1, 0).getDate(); const diaSemanaInicio = new Date(año, mes, 1).getDay(); const hoyStr = fechaToStr(hoy); const resumen = useMemo(() => { const r = {}; for (let d = 1; d <= diasDelMes; d++) { const f = año + “-” + String(mes + 1).padStart(2, “0”) + “-” + String(d).padStart(2, “0”); const conteo = {}; for (const col of colaboradores) { const t = obtenerTurno(col.id, f); if (t) conteo[t] = (conteo[t] || 0) + 1; } const dow = new Date(año, mes, d).getDay(); const violaciones = []; for (const m of minimos) { if (m.dia !== “todos” && Number(m.dia) !== dow) continue; let cnt = 0; for (const col of colaboradores) { if (m.rol && m.rol !== “todos” && col.rol !== m.rol) continue; if (obtenerTurno(col.id, f) === m.turno) cnt++; } if (cnt < m.cantidad) violaciones.push({ turno: m.turno, cantidad: m.cantidad, actual: cnt, rol: m.rol }); } r[f] = { conteo, violaciones }; } return r; }, [año, mes, diasDelMes, colaboradores, minimos]); const celdas = []; for (let i = 0; i < diaSemanaInicio; i++) celdas.push(
); for (let d = 1; d <= diasDelMes; d++) { const f = año + “-” + String(mes + 1).padStart(2, “0”) + “-” + String(d).padStart(2, “0”); const datos = resumen[f] || { conteo: {}, violaciones: [] }; const esHoy = f === hoyStr; const tieneAlerta = datos.violaciones.length > 0; let claseFondo = “border-gray-200 bg-white hover:bg-gray-50 active:bg-gray-100”; if (esHoy) claseFondo = “border-indigo-400 bg-indigo-50”; else if (tieneAlerta) claseFondo = “border-red-300 bg-red-50”; const claseNumero = esHoy ? “text-indigo-700” : “text-gray-500”; ``` celdas.push(  
setDiaAbierto(f)} className={"min-h-[60px] sm:min-h-[80px] p-1.5 rounded-lg cursor-pointer border transition hover:shadow-md " + claseFondo}>    
      {d}       {tieneAlerta && }    
    {Object.keys(datos.conteo).map((t) => (      
       
        {datos.conteo[t]}      
    ))}  
); ``` } return (

{MESES[mes]} {año}

{DIAS_CORTOS.map((d) =>
{d}
)}
{celdas}
{Object.keys(TURNOS).map((k) => ( {TURNOS[k].label} ))} ⚠ Mínimo incumplido
{diaAbierto && ( setDiaAbierto(null)} /> )}
); } function ModalDia({ fecha, colaboradores, equipos, asignaciones, setAsignaciones, violaciones, obtenerTurno, onClose }) { const [busqueda, setBusqueda] = useState(””); const [filtroEq, setFiltroEq] = useState(””); const d = strToFecha(fecha); function cambiarTurno(colId, turno) { const clave = colId + “_” + fecha; const nuevo = { …asignaciones }; if (turno) nuevo[clave] = turno; else delete nuevo[clave]; setAsignaciones(nuevo); } const lista = colaboradores.filter((c) => { if (busqueda && !c.nombre.toLowerCase().includes(busqueda.toLowerCase())) return false; if (filtroEq && c.eqId !== filtroEq) return false; return true; }); const titulo = DIAS_LARGOS[d.getDay()] + “ “ + d.getDate() + “ “ + MESES[d.getMonth()] + “ “ + d.getFullYear(); return ( {violaciones.length > 0 && (
⚠️ {violaciones.map((v) => { const rolStr = v.rol !== “todos” ? “ (” + v.rol + “)” : “”; return TURNOS[v.turno].label + “: “ + v.actual + “/” + v.cantidad + rolStr; }).join(”  ·  “)}
)}
setBusqueda(e.target.value)} placeholder=“Buscar…” className=“border border-gray-300 rounded-lg px-2 py-2 text-sm flex-1” />
{lista.map((col) => { const turno = obtenerTurno(col.id, fecha); const tieneOverride = !!asignaciones[col.id + “_” + fecha]; const eq = equipos.find((e) => e.id === col.eqId); const claseFondo = tieneOverride ? “bg-amber-50 border border-amber-200” : “hover:bg-gray-50 border border-transparent”; const subtexto = [col.rol, eq ? eq.nombre : null].filter(Boolean).join(” · “); return (
{col.nombre.slice(0, 2).toUpperCase()}

{col.nombre}

{subtexto}

cambiarTurno(col.id, t)} /> {tieneOverride && ( )}
); })} {lista.length === 0 &&

Sin colaboradores

}
); } function TabColaboradores({ colaboradores, setColaboradores, equipos, ciclos, aplicaciones, setAplicaciones }) { const [busqueda, setBusqueda] = useState(””); const [filtroEq, setFiltroEq] = useState(””); const [filtroRol, setFiltroRol] = useState(””); const [formAbierto, setFormAbierto] = useState(false); const [editando, setEditando] = useState(null); const [cicloPara, setCicloPara] = useState(null); const [bulkAbierto, setBulkAbierto] = useState(false); const roles = […new Set(colaboradores.map((c) => c.rol).filter(Boolean))]; const lista = colaboradores.filter((c) => { if (busqueda && !c.nombre.toLowerCase().includes(busqueda.toLowerCase())) return false; if (filtroEq && c.eqId !== filtroEq) return false; if (filtroRol && c.rol !== filtroRol) return false; return true; }); function eliminar(id) { if (!confirm(”¿Eliminar colaborador?”)) return; setColaboradores(colaboradores.filter((c) => c.id !== id)); setAplicaciones(aplicaciones.filter((a) => a.colId !== id)); } return (

👥 Colaboradores ({colaboradores.length})

setBulkAbierto(true)}>📋 Importar { setEditando(null); setFormAbierto(true); }}>+ Agregar
setBusqueda(e.target.value)} placeholder=“Buscar…” className=“border border-gray-300 rounded-lg px-3 py-2 text-sm flex-1 min-w-32” />
{lista.map((col) => { const eq = equipos.find((e) => e.id === col.eqId); const apsCol = aplicaciones.filter((a) => a.colId === col.id).sort((a, b) => b.inicio.localeCompare(a.inicio)); const ap = apsCol[0]; const ciclo = ap ? ciclos.find((c) => c.id === ap.cicloId) : null; return (
{col.nombre.slice(0, 2).toUpperCase()}

{col.nombre}

{col.rol && {col.rol}} {eq && {eq.nombre}} {ciclo && 🔄 {ciclo.nombre}}
setCicloPara(col.id)}>Ciclo { setEditando(col); setFormAbierto(true); }}>✏️ eliminar(col.id)}>🗑
); })} {lista.length === 0 &&

Sin resultados

}
{formAbierto && ( setFormAbierto(false)} /> )} {cicloPara && ( setCicloPara(null)} /> )} {bulkAbierto && ( setBulkAbierto(false)} /> )}
); } function FormColaborador({ item, colaboradores, setColaboradores, equipos, onClose }) { const [nombre, setNombre] = useState(item ? item.nombre : “”); const [rol, setRol] = useState(item ? item.rol : “”); const [eqId, setEqId] = useState(item ? item.eqId : “”); function guardar() { if (!nombre.trim()) return; if (item) { setColaboradores(colaboradores.map((c) => c.id === item.id ? { …c, nombre, rol, eqId } : c)); } else { setColaboradores([…colaboradores, { id: nuevoId(), nombre, rol, eqId }]); } onClose(); } return (
setNombre(e.target.value)} className={ESTILO_INPUT} placeholder=“Nombre completo” /> setRol(e.target.value)} className={ESTILO_INPUT} placeholder=“Ej: Enfermero, Vigilante…” />
{item ? “Guardar” : “Crear”} Cancelar
); } function FormImportar({ colaboradores, setColaboradores, equipos, onClose }) { const [texto, setTexto] = useState(””); const [rol, setRol] = useState(””); const [eqId, setEqId] = useState(””); const nombres = texto.split(”\n”).map((s) => s.trim()).filter((s) => s.length > 0); function guardar() { if (nombres.length === 0) return; setColaboradores([…colaboradores, …nombres.map((n) => ({ id: nuevoId(), nombre: n, rol, eqId }))]); onClose(); } return (

Pega un nombre por línea. El rol y equipo se aplican a todos.