/* App shell — sidebar, tabs, estado, persistencia, CRUD, tweaks */
const { useState, useEffect, useRef } = React;

const STORE_KEY = "cl_events_v2";

/* ============================================================
   Importación desde Excel
   Hoja "Solicitudes": Código | Nombre | Color (opcional)
   Hoja "Marcas":      Recurso | Tipo | Código Solicitud | Estado | Fecha Inicio | Fecha Fin
   ============================================================ */
function _normTxt(s) { return String(s == null ? "" : s).trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, ""); }

function _toISOValue(v) {
  if (v == null || v === "") return "";
  if (v instanceof Date && !isNaN(v.getTime())) return window.CL.iso(v);
  if (typeof v === "number" && typeof XLSX !== "undefined" && XLSX.SSF) {
    const d = XLSX.SSF.parse_date_code(v);
    if (d) return d.y + "-" + String(d.m).padStart(2, "0") + "-" + String(d.d).padStart(2, "0");
  }
  const s = String(v).trim();
  if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(s)) {
    const [y, m, d] = s.split("-");
    return y + "-" + m.padStart(2, "0") + "-" + d.padStart(2, "0");
  }
  const m = s.match(/^(\d{1,2})[\/\.\-](\d{1,2})[\/\.\-](\d{2,4})$/);
  if (m) {
    let y = m[3]; if (y.length === 2) y = (+y > 50 ? "19" : "20") + y;
    return y + "-" + m[2].padStart(2, "0") + "-" + m[1].padStart(2, "0");
  }
  const dt = new Date(s);
  if (!isNaN(dt.getTime())) return window.CL.iso(dt);
  return "";
}

/* ============================================================
   Importación de Jefes de Proyecto + Recursos desde Excel
   Hoja sugerida: "Equipos" o "Recursos"
   Columnas: Jefe de Proyecto | Equipo | Recurso (opc) | Rol (opc)
   Empareja equipos por nombre (no distingue mayúsculas ni tildes).
   ============================================================ */
function importTeamsFromWorkbook(wb) {
  const CL = window.CL;
  let addedTeams = 0, addedResources = 0, skipped = 0;
  const errors = [];
  const sheetName = wb.SheetNames.find((n) => /equip|jefe|recurs|team/i.test(n)) || wb.SheetNames[0];
  if (!sheetName) { errors.push("El archivo Excel no tiene hojas."); return { addedTeams, addedResources, skipped, errors }; }

  const teamByEquipo = new Map(CL.TEAMS.map((t) => [_normTxt(t.team), t]));
  const ROL_MAP = {
    "desarrollador": "Desarrollador", "dev": "Desarrollador", "developer": "Desarrollador", "programador": "Desarrollador",
    "analista qa": "Analista QA", "qa": "Analista QA", "tester": "Analista QA", "testing": "Analista QA", "calidad": "Analista QA",
    "analista prod.": "Analista Prod.", "analista prod": "Analista Prod.", "analista produccion": "Analista Prod.", "produccion": "Analista Prod.", "prod": "Analista Prod.",
  };
  function findRole(raw) {
    const n = _normTxt(raw); if (!n) return "Desarrollador";
    if (ROL_MAP[n]) return ROL_MAP[n];
    for (const k of Object.keys(ROL_MAP)) if (n.includes(k)) return ROL_MAP[k];
    return "Desarrollador";
  }

  const rows = XLSX.utils.sheet_to_json(wb.Sheets[sheetName], { header: 1, defval: "", raw: true });
  let h = null;
  rows.forEach((row, i) => {
    if (!Array.isArray(row) || row.every((c) => c === "" || c == null)) return;
    if (!h) {
      const low = row.map(_normTxt);
      const idxOf = (...keys) => { for (const k of keys) { const j = low.findIndex((c) => c.includes(k)); if (j >= 0) return j; } return -1; };
      const pm = idxOf("jefe", "pm", "manager");
      const eq = idxOf("equipo", "team");
      const re = idxOf("recurso", "colaborador");
      const rl = idxOf("rol", "cargo", "puesto");
      if (pm >= 0 && eq >= 0) h = { pm, eq, re, rl };
      return;
    }
    const rowNum = i + 1;
    const pmName = String(row[h.pm] || "").trim();
    const equipo = String(row[h.eq] || "").trim();
    if (!pmName || !equipo) { errors.push("Fila " + rowNum + ": faltan Jefe de Proyecto o Equipo."); skipped++; return; }
    let team = teamByEquipo.get(_normTxt(equipo));
    if (!team) {
      const id = CL.uid("PM");
      team = { id, pm: pmName, team: equipo, resources: [] };
      CL.TEAMS.push(team);
      teamByEquipo.set(_normTxt(equipo), team);
      addedTeams++;
    } else if (team.pm !== pmName) {
      team.pm = pmName;
    }
    const resName = h.re >= 0 ? String(row[h.re] || "").trim() : "";
    if (resName) {
      if (team.resources.some((r) => _normTxt(r.name) === _normTxt(resName))) return;
      const role = findRole(h.rl >= 0 ? row[h.rl] : "");
      const rp = CL.ROLES.find((r) => r.role === role) || CL.ROLES[0];
      team.resources.push({
        id: CL.uid(team.id + "-R"),
        name: resName,
        initials: CL.initialsOf(resName),
        role,
        roleShort: rp.roleShort,
        pmId: team.id,
      });
      addedResources++;
    }
  });
  if (!h) errors.push("La hoja '" + sheetName + "' no tiene la cabecera esperada (Jefe de Proyecto, Equipo, Recurso, Rol).");
  if (addedTeams || addedResources) CL.rebuildIndexes();
  return { addedTeams, addedResources, skipped, errors };
}

function importFromWorkbook(wb) {
  const CL = window.CL;
  const newProjects = [];
  const updatedProjectIds = [];
  const addedMarcas = [];
  const errors = [];
  const findSheet = (re) => wb.SheetNames.find((n) => re.test(_normTxt(n)));

  // ----- Hoja Solicitudes -----
  const existingIds = new Set(CL.PROJECTS.map((p) => p.id));
  const solSheet = findSheet(/solicit|proyect/);
  if (solSheet) {
    const rows = XLSX.utils.sheet_to_json(wb.Sheets[solSheet], { header: 1, defval: "", raw: true });
    let h = null;
    rows.forEach((row) => {
      if (!Array.isArray(row) || row.every((c) => c === "" || c == null)) return;
      if (!h) {
        const low = row.map(_normTxt);
        const idxOf = (...keys) => { for (const k of keys) { const j = low.findIndex((c) => c.includes(k)); if (j >= 0) return j; } return -1; };
        const cod = idxOf("codigo", "cod");
        const num = idxOf("numero", "n°", "n.°", "n ", "nro");
        const nom = idxOf("nombre");
        const div = idxOf("division");
        const dep = idxOf("departamento", "depto", "depart");
        const col = idxOf("color");
        if (cod >= 0 && nom >= 0) h = { cod, num, nom, div, dep, col };
        return;
      }
      const cod = String(row[h.cod] || "").trim().toUpperCase();
      const nom = String(row[h.nom] || "").trim();
      if (!cod || !nom) return;
      const numero = h.num >= 0 ? (Number(row[h.num]) || 0) : null;
      const division = h.div >= 0 ? String(row[h.div] || "").trim() : null;
      const departamento = h.dep >= 0 ? String(row[h.dep] || "").trim() : null;
      const colHex = h.col >= 0 ? String(row[h.col] || "").trim() : "";
      const validColor = /^#[0-9A-Fa-f]{6}$/.test(colHex);

      const existing = CL.PROJECTS.find((p) => p.id === cod);
      if (existing) {
        // actualizar: nombre + cualquier campo presente en el archivo
        existing.name = nom;
        if (numero !== null) existing.numero = numero;
        if (division !== null) existing.division = division;
        if (departamento !== null) existing.departamento = departamento;
        if (validColor) existing.color = colHex;
        updatedProjectIds.push(cod);
      } else {
        const used = new Set(CL.PROJECTS.map((p) => p.color).concat(newProjects.map((p) => p.color)));
        const color = validColor ? colHex : (CL.PROJECT_COLORS.find((c) => !used.has(c)) || CL.PROJECT_COLORS[(existingIds.size + newProjects.length) % CL.PROJECT_COLORS.length]);
        newProjects.push({ id: cod, numero: numero || 0, name: nom, color, division: division || "", departamento: departamento || "" });
        existingIds.add(cod);
      }
    });
  }

  // ----- Hoja Marcas -----
  const mkSheet = findSheet(/marc|asign|carga/);
  if (!mkSheet) {
    errors.push("No se encontró la hoja 'Marcas' en el archivo.");
    return { newProjects, addedMarcas, skipped: 0, errors };
  }
  const resourceByName = new Map();
  CL.ALL_RESOURCES.forEach((r) => resourceByName.set(_normTxt(r.name), r));
  const projById = new Map(CL.PROJECTS.concat(newProjects).map((p) => [p.id, p]));
  const TYPE = { "asignacion": "asignacion", "asignar": "asignacion", "proyecto": "asignacion", "vacaciones": "vacaciones", "vacacion": "vacaciones", "licencia": "licencia", "permiso": "licencia", "capacitacion": "capacitacion", "capac": "capacitacion" };
  const EST = { "en curso": "en_curso", "en_curso": "en_curso", "curso": "en_curso", "completada": "completada", "completado": "completada", "a tiempo": "completada", "en riesgo": "en_riesgo", "en_riesgo": "en_riesgo", "riesgo": "en_riesgo", "retrasada": "retrasada", "retrasado": "retrasada", "retraso": "retrasada" };
  function lookupKey(map, raw) {
    const n = _normTxt(raw); if (!n) return null;
    if (map[n]) return map[n];
    for (const k of Object.keys(map)) if (n.includes(k)) return map[k];
    return null;
  }

  const rows = XLSX.utils.sheet_to_json(wb.Sheets[mkSheet], { header: 1, defval: "", raw: true });
  let h = null, skipped = 0;
  rows.forEach((row, i) => {
    if (!Array.isArray(row) || row.every((c) => c === "" || c == null)) return;
    if (!h) {
      const low = row.map(_normTxt);
      const idxOf = (...keys) => { for (const k of keys) { const j = low.findIndex((c) => c.includes(k)); if (j >= 0) return j; } return -1; };
      const r = idxOf("recurso", "nombre"), t = idxOf("tipo"), c = idxOf("codigo", "solicitud"), e = idxOf("estado"), s = idxOf("inicio"), f = idxOf("fin");
      if (r >= 0 && t >= 0 && s >= 0 && f >= 0) h = { r, t, c, e, s, f };
      return;
    }
    const rowNum = i + 1;
    const rname = String(row[h.r] || "").trim();
    if (!rname) return;
    const res = resourceByName.get(_normTxt(rname));
    if (!res) { errors.push("Fila " + rowNum + ": recurso \"" + rname + "\" no existe."); skipped++; return; }
    const type = lookupKey(TYPE, row[h.t]);
    if (!type) { errors.push("Fila " + rowNum + ": tipo \"" + row[h.t] + "\" no reconocido."); skipped++; return; }
    const start = _toISOValue(row[h.s]);
    const end = _toISOValue(row[h.f]);
    if (!start || !end) { errors.push("Fila " + rowNum + ": fechas inválidas."); skipped++; return; }
    if (CL.parseISO(end).getTime() < CL.parseISO(start).getTime()) { errors.push("Fila " + rowNum + ": fecha fin anterior al inicio."); skipped++; return; }
    const y0 = CL.parseISO(start).getFullYear(), y1 = CL.parseISO(end).getFullYear();
    if (y0 < CL.MIN_YEAR || y1 > CL.MAX_YEAR) { errors.push("Fila " + rowNum + ": año fuera de rango (" + CL.MIN_YEAR + "–" + CL.MAX_YEAR + ")."); skipped++; return; }
    const ev = { id: CL.uid("X") + "_" + i, resourceId: res.id, type, start, end };
    if (type === "asignacion") {
      const code = String(row[h.c] || "").trim().toUpperCase();
      if (!code) { errors.push("Fila " + rowNum + ": asignación sin código de solicitud."); skipped++; return; }
      let proj = projById.get(code);
      if (!proj) {
        const used = new Set(CL.PROJECTS.map((p) => p.color).concat(newProjects.map((p) => p.color)));
        const color = CL.PROJECT_COLORS.find((c) => !used.has(c)) || CL.PROJECT_COLORS[(CL.PROJECTS.length + newProjects.length) % CL.PROJECT_COLORS.length];
        proj = { id: code, name: code, color };
        newProjects.push(proj); projById.set(code, proj);
      }
      ev.projectId = code; ev.title = proj.name;
      const estado = lookupKey(EST, row[h.e]) || "en_curso";
      ev.estado = estado; ev.planEnd = end;
      if (estado === "completada" || estado === "retrasada") { ev.realEnd = end; ev.onTime = estado === "completada"; }
      else { ev.realEnd = null; ev.onTime = null; }
    } else {
      ev.title = CL.EVENT_TYPES[type].label;
    }
    addedMarcas.push(ev);
  });
  if (!h) errors.push("La hoja '" + mkSheet + "' no tiene la cabecera esperada (Recurso, Tipo, Código Solicitud, Estado, Fecha Inicio, Fecha Fin).");
  return { newProjects, updatedProjectIds, addedMarcas, skipped, errors };
}

// Cargar configuración (equipos / solicitudes) antes de render
window.CL.loadConfig();

function loadEvents() {
  let evs;
  try {
    const raw = localStorage.getItem(STORE_KEY);
    evs = raw ? JSON.parse(raw) : window.CL.EVENTS.map((e) => Object.assign({}, e));
  } catch (e) { evs = window.CL.EVENTS.map((e) => Object.assign({}, e)); }
  // descartar marcas de recursos inexistentes, y asignaciones cuya solicitud ya no existe
  return evs.filter((e) => {
    if (!window.CL.RESOURCE_MAP.has(e.resourceId)) return false;
    if (e.type === "asignacion" && e.projectId && !window.CL.PROJECT_MAP.has(e.projectId)) return false;
    return true;
  });
}
function saveEvents(evs) { try { localStorage.setItem(STORE_KEY, JSON.stringify(evs)); } catch (e) {} }

function Tooltip({ tip }) {
  if (!tip) return null;
  return <div className="tip" style={{ left: tip.x, top: tip.y }} dangerouslySetInnerHTML={{ __html: tip.html }}></div>;
}

const THEMES = {
  balanceado: { a: "#E11D2A", a2: "#7C3AED", soft: "#F6E9F2" },
  rojo: { a: "#E11D2A", a2: "#B8154A", soft: "#FCE7E9" },
  morado: { a: "#7C3AED", a2: "#5B21B6", soft: "#EDE9FE" },
};

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "tema": "balanceado",
  "vivo": true
}/*EDITMODE-END*/;

function defaultScope() {
  const CL = window.CL;
  if (CL.RESOURCE_MAP.has("PM1-R1")) return { type: "resource", id: "PM1-R1" };
  if (CL.ALL_RESOURCES.length) return { type: "resource", id: CL.ALL_RESOURCES[0].id };
  if (CL.TEAMS.length) return { type: "team", id: CL.TEAMS[0].id };
  return { type: "all" };
}

function Sidebar({ scope, setScope, openTeams, toggleTeam, year, collapsed, onToggleCollapse, onNewPM, onEditPM, onDeletePM, onNewResource, onEditResource, onDeleteResource }) {
  const CL = window.CL;
  if (collapsed) {
    return (
      <div className="sidebar sidebar-c">
        <button className="collapse-toggle c" onClick={onToggleCollapse} title="Expandir menú">{Icon.chevR({ width: 18, height: 18 })}</button>
        <div className="brand-c">
          <div className="brand-mark">
            <svg viewBox="0 0 24 24" fill="none"><path d="M4 13l4 4 12-12" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"/><path d="M4 19h16" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" opacity=".55"/></svg>
          </div>
        </div>
        <div className="side-scroll-c">
          <button className={"icon-side" + (scope.type === "all" ? " active" : "")} onClick={() => setScope({ type: "all" })} title={"Todos los equipos (" + CL.ALL_RESOURCES.length + ")"}>
            {Icon.users({ width: 18, height: 18 })}
          </button>
          <div className="icon-side-divider"></div>
          {CL.TEAMS.map((t) => (
            <button key={t.id} className={"icon-team" + (scope.type === "team" && scope.id === t.id ? " active" : "")} onClick={() => setScope({ type: "team", id: t.id })} title={t.pm + " · Equipo " + t.team + " (" + t.resources.length + " recursos)"}>
              <span className="icon-team-dot" style={{ background: avatarColor(t.id) }}></span>
              <span className="icon-team-i">{(t.pm || "").trim().split(/\s+/).map((p) => p[0]).slice(0, 2).join("").toUpperCase()}</span>
            </button>
          ))}
          <button className="icon-side icon-add" title="Nuevo Jefe de Proyecto" onClick={onNewPM}>{Icon.plus({ width: 16, height: 16 })}</button>
        </div>
      </div>
    );
  }
  return (
    <div className="sidebar">
      <button className="collapse-toggle" onClick={onToggleCollapse} title="Contraer menú">{Icon.chevL({ width: 16, height: 16 })}</button>
      <div className="brand">
        <div className="brand-mark">
          <svg viewBox="0 0 24 24" fill="none"><path d="M4 13l4 4 12-12" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"/><path d="M4 19h16" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" opacity=".55"/></svg>
        </div>
        <div>
          <div className="brand-title">Capacidad TI</div>
          <div className="brand-sub">Lima, Perú · {year}</div>
        </div>
      </div>
      <div className="side-scroll">
        <button className={"allbtn" + (scope.type === "all" ? " active" : "")} onClick={() => setScope({ type: "all" })}>
          {Icon.users({ width: 17, height: 17 })} Todos los equipos
          <span style={{ marginLeft: "auto", fontSize: 11, opacity: .7 }}>{CL.ALL_RESOURCES.length}</span>
        </button>

        <div className="section-row">
          <div className="side-section-label">Jefes de Proyecto</div>
          <button className="side-add" title="Nuevo Jefe de Proyecto" onClick={onNewPM}>{Icon.plus({})}</button>
        </div>

        {CL.TEAMS.map((t) => {
          const open = openTeams.has(t.id);
          return (
            <div className="team" key={t.id}>
              <div className="row-wrap">
                <button className={"team-head" + (open ? " open" : "") + (scope.type === "team" && scope.id === t.id ? " active" : "")}
                  onClick={() => { setScope({ type: "team", id: t.id }); toggleTeam(t.id); }} style={{ width: "100%" }}>
                  <span className="team-dot" style={{ background: avatarColor(t.id) }}></span>
                  <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{t.pm}</span>
                  <span className="team-meta">{t.team}</span>
                  <span className="chev" onClick={(e) => { e.stopPropagation(); toggleTeam(t.id); }}>{Icon.chevR({ width: 15, height: 15 })}</span>
                </button>
                <div className="side-actions">
                  <button className="icon-act" title="Editar equipo" onClick={(e) => { e.stopPropagation(); onEditPM(t); }}>{Icon.pencil({})}</button>
                  <button className="icon-act danger" title="Eliminar equipo" onClick={(e) => { e.stopPropagation(); onDeletePM(t); }}>{Icon.trash({})}</button>
                </div>
              </div>
              {open && (
                <div className="res-list">
                  {t.resources.map((r) => (
                    <div className="row-wrap" key={r.id}>
                      <button className={"res" + (scope.type === "resource" && scope.id === r.id ? " active" : "")}
                        onClick={() => setScope({ type: "resource", id: r.id })} style={{ width: "100%" }}>
                        <span className="avatar" style={{ background: avatarColor(r.id) }}>{r.initials}</span>
                        <span className="res-name">{r.name}</span>
                        <span className="res-role">{r.roleShort}</span>
                      </button>
                      <div className="side-actions">
                        <button className="icon-act" title="Editar recurso" onClick={(e) => { e.stopPropagation(); onEditResource(r, t); }}>{Icon.pencil({})}</button>
                        <button className="icon-act danger" title="Eliminar recurso" onClick={(e) => { e.stopPropagation(); onDeleteResource(r, t); }}>{Icon.trash({})}</button>
                      </div>
                    </div>
                  ))}
                  <button className="add-res-btn" onClick={() => onNewResource(t)}>{Icon.plus({})} Agregar recurso</button>
                </div>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

function App() {
  const CL = window.CL;
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [events, setEvents] = useState(loadEvents);
  const [scope, setScope] = useState(defaultScope);
  const [tab, setTab] = useState("calendario");
  const [year, setYear] = useState(CL.YEAR);
  const [month, setMonth] = useState(0);
  const [openTeams, setOpenTeams] = useState(new Set());
  const [expandedCentralTeams, setExpandedCentralTeams] = useState(new Set());
  const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
    try { return localStorage.getItem("cl_sidebar_collapsed_v1") === "1"; } catch (e) { return false; }
  });
  function toggleSidebar() {
    setSidebarCollapsed((c) => {
      const next = !c;
      try { localStorage.setItem("cl_sidebar_collapsed_v1", next ? "1" : "0"); } catch (e) {}
      return next;
    });
  }
  const [modal, setModal] = useState(null);
  const [tip, setTip] = useState(null);
  const [rev, setRev] = useState(0);
  const bump = () => setRev((r) => r + 1);
  const [menuOpen, setMenuOpen] = useState(false);
  const fileRef = useRef(null);
  const excelRef = useRef(null);
  const teamsExcelRef = useRef(null);
  const [toast, setToast] = useState(null);
  const toastRef = useRef(null);
  function showToast(msg) { setToast(msg); if (toastRef.current) clearTimeout(toastRef.current); toastRef.current = setTimeout(() => setToast(null), 2200); }
  const [alerts, setAlerts] = useState([]);
  const [alertOpen, setAlertOpen] = useState(false);
  const ALERTS_KEY = "cl_alerts_v1";

  // Cargar alertas guardadas del día al iniciar
  useEffect(() => {
    try {
      const raw = localStorage.getItem(ALERTS_KEY);
      const today = window.CLA.todayISO();
      if (raw) {
        const stored = JSON.parse(raw);
        if (stored && stored.date === today && Array.isArray(stored.alerts)) setAlerts(stored.alerts);
        else { localStorage.removeItem(ALERTS_KEY); setAlerts([]); }
      }
    } catch (e) {}
  }, []);

  // Auto-transición en_curso → en_riesgo cuando faltan ≤ 3 días hábiles
  useEffect(() => {
    if (!window.CLA || !events) return;
    const today = window.CLA.todayISO();
    const res = window.CLA.computeAtRisk(events, today);
    if (!res.changed) return;
    setEvents(res.updatedEvents);
    setAlerts((prev) => {
      const seenIds = new Set(prev.map((a) => a.eventId));
      const fresh = res.newAlerts.filter((a) => !seenIds.has(a.eventId));
      if (!fresh.length) return prev;
      const next = prev.concat(fresh);
      try { localStorage.setItem(ALERTS_KEY, JSON.stringify({ date: today, alerts: next })); } catch (e) {}
      return next;
    });
  }, [events]);

  // Cerrar panel de alertas al hacer click fuera
  useEffect(() => {
    if (!alertOpen) return;
    const h = (e) => { if (!e.target.closest(".alert-wrap")) setAlertOpen(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [alertOpen]);

  useEffect(() => {
    if (!menuOpen) return;
    const h = (e) => { if (!e.target.closest(".data-menu-wrap")) setMenuOpen(false); };
    document.addEventListener("mousedown", h);
    return () => document.removeEventListener("mousedown", h);
  }, [menuOpen]);

  useEffect(() => { saveEvents(events); }, [events]);
  useEffect(() => {
    const th = THEMES[t.tema] || THEMES.balanceado;
    const root = document.documentElement.style;
    root.setProperty("--accent", th.a); root.setProperty("--accent-2", th.a2); root.setProperty("--accent-soft", th.soft);
    document.body.classList.toggle("flat", !t.vivo);
  }, [t.tema, t.vivo]);
  // validar scope si se eliminó la entidad
  useEffect(() => {
    if (scope.type === "resource" && !CL.RESOURCE_MAP.has(scope.id)) setScope({ type: "all" });
    if (scope.type === "team" && !CL.TEAMS.find((x) => x.id === scope.id)) setScope({ type: "all" });
  }, [rev]);

  function toggleTeam(id) { setOpenTeams((p) => { const n = new Set(p); n.has(id) ? n.delete(id) : n.add(id); return n; }); }
  const close = () => { const ret = modal?.returnTo; setModal(ret ? { kind: ret } : null); };

  // navegación de calendario (mes rueda entre años)
  const canPrev = year > CL.MIN_YEAR || month > 0;
  const canNext = year < CL.MAX_YEAR || month < 11;
  function prevMonth() { if (month === 0) { if (year > CL.MIN_YEAR) { setYear(year - 1); setMonth(11); } } else setMonth(month - 1); }
  function nextMonth() { if (month === 11) { if (year < CL.MAX_YEAR) { setYear(year + 1); setMonth(0); } } else setMonth(month + 1); }
  function changeYear(y) { const ny = Math.max(CL.MIN_YEAR, Math.min(CL.MAX_YEAR, y)); setYear(ny); }

  const resource = scope.type === "resource" ? CL.RESOURCE_MAP.get(scope.id) : null;
  const resourceIds = scope.type === "all" ? CL.ALL_RESOURCES.map((r) => r.id)
    : scope.type === "team" ? (CL.TEAMS.find((x) => x.id === scope.id)?.resources || []).map((r) => r.id)
    : [scope.id];

  const curTeam = scope.type === "team" ? CL.TEAMS.find((x) => x.id === scope.id) : (resource ? CL.TEAMS.find((x) => x.id === resource.pmId) : null);
  const scopeName = scope.type === "all" ? "Todos los equipos"
    : scope.type === "team" ? "Equipo " + curTeam?.team + " · " + curTeam?.pm
    : resource ? resource.name : "—";
  const scopeSub = scope.type === "all" ? CL.ALL_RESOURCES.length + " recursos · " + CL.TEAMS.length + " jefes de proyecto"
    : scope.type === "team" ? curTeam?.resources.length + " recursos · " + curTeam?.pm
    : resource ? resource.role + " · Equipo " + curTeam?.team : "";

  // ----- Eventos (marcas) -----
  function openAdd() {
    const start = CL.iso(CL.nextWorkingFrom(new Date(year, month, 1)));
    const end = CL.endAfterWorkingDays(start, 5);
    setModal({ kind: "event", initial: { type: "asignacion", start, end } });
  }
  function openEdit(ev) { setModal({ kind: "event", initial: ev }); }
  function saveEvent(ev) {
    setEvents((prev) => ev.id ? prev.map((e) => (e.id === ev.id ? ev : e)) : prev.concat([Object.assign({}, ev, { id: "U" + Date.now() })]));
    showToast(ev.id ? "Marca guardada" : "Marca agregada");
    close();
  }
  function deleteEvent(id) { setEvents((prev) => prev.filter((e) => e.id !== id)); showToast("Marca eliminada"); close(); }

  // ----- CRUD Jefe de Proyecto -----
  function savePM(d) {
    if (d.id) { const tm = CL.TEAMS.find((x) => x.id === d.id); if (tm) { tm.pm = d.pm; tm.team = d.team; } }
    else {
      const id = CL.uid("PM");
      const team = { id, pm: d.pm, team: d.team, resources: [] };
      if (d.template) {
        const plan = [["Desarrollador", "Dev", "Desarrollador 1"], ["Desarrollador", "Dev", "Desarrollador 2"], ["Desarrollador", "Dev", "Desarrollador 3"], ["Analista QA", "QA", "Analista QA"], ["Analista Prod.", "Prod", "Analista Producción"]];
        plan.forEach((p) => team.resources.push({ id: CL.uid(id + "-R"), name: p[2], initials: CL.initialsOf(p[2]), role: p[0], roleShort: p[1], pmId: id }));
      }
      CL.TEAMS.push(team);
      setOpenTeams((prev) => new Set(prev).add(id));
      setScope({ type: "team", id });
    }
    CL.rebuildIndexes(); CL.saveConfig(); bump(); close();
    showToast(d.id ? "Jefe de Proyecto guardado" : "Equipo creado");
  }
  function deletePM(team) {
    if (!window.confirm(`¿Eliminar el equipo "${team.team}" (${team.pm}) y sus ${team.resources.length} recursos? También se borrarán sus marcas.`)) return;
    const ids = new Set(team.resources.map((r) => r.id));
    CL.TEAMS = CL.TEAMS.filter((x) => x.id !== team.id);
    setEvents((prev) => prev.filter((e) => !ids.has(e.resourceId)));
    if ((scope.type === "team" && scope.id === team.id) || (scope.type === "resource" && ids.has(scope.id))) setScope({ type: "all" });
    CL.rebuildIndexes(); CL.saveConfig(); bump(); close();
    showToast("Equipo eliminado");
  }

  // ----- CRUD Recurso -----
  function saveResource(d, teamId) {
    const tm = CL.TEAMS.find((x) => x.id === teamId); if (!tm) return;
    const rp = CL.ROLES.find((r) => r.role === d.role) || CL.ROLES[0];
    if (d.id) { const r = tm.resources.find((x) => x.id === d.id); if (r) { r.name = d.name; r.role = d.role; r.roleShort = rp.roleShort; r.initials = CL.initialsOf(d.name); } }
    else {
      const id = CL.uid(teamId + "-R");
      tm.resources.push({ id, name: d.name, initials: CL.initialsOf(d.name), role: d.role, roleShort: rp.roleShort, pmId: teamId });
      setScope({ type: "resource", id });
      setOpenTeams((prev) => new Set(prev).add(teamId));
    }
    CL.rebuildIndexes(); CL.saveConfig(); bump(); close();
    showToast(d.id ? "Recurso guardado" : "Recurso agregado");
  }
  function deleteResource(res, team) {
    if (!window.confirm(`¿Eliminar a "${res.name}" del equipo ${team.team}? También se borrarán sus marcas.`)) return;
    const tm = CL.TEAMS.find((x) => x.id === team.id); if (!tm) return;
    tm.resources = tm.resources.filter((r) => r.id !== res.id);
    setEvents((prev) => prev.filter((e) => e.resourceId !== res.id));
    if (scope.type === "resource" && scope.id === res.id) setScope({ type: "team", id: team.id });
    CL.rebuildIndexes(); CL.saveConfig(); bump(); close();
    showToast("Recurso eliminado");
  }

  // ----- CRUD Solicitudes -----
  function saveProjects({ newProjects, remap, removed }) {
    CL.PROJECTS = newProjects;
    const removedSet = new Set(removed);
    setEvents((prev) => prev
      .filter((e) => !(e.type === "asignacion" && removedSet.has(e.projectId)))
      .map((e) => {
        if (e.type !== "asignacion") return e;
        let pid = e.projectId;
        if (remap[pid]) pid = remap[pid];
        const proj = newProjects.find((p) => p.id === pid);
        return Object.assign({}, e, { projectId: pid, title: proj ? proj.name : e.title });
      }));
    CL.rebuildIndexes(); CL.saveConfig(); bump(); close();
    showToast("Solicitudes actualizadas");
  }

  function pickResource(id) { setScope({ type: "resource", id }); const r = CL.RESOURCE_MAP.get(id); if (r) setOpenTeams((p) => new Set(p).add(r.pmId)); setTab("calendario"); }

  // ----- Exportar / Importar / Restablecer -----
  function downloadBlob(blob, name) {
    const url = URL.createObjectURL(blob); const a = document.createElement("a");
    a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }
  function exportExcel() {
    setMenuOpen(false);
    if (typeof XLSX === "undefined") { exportCsvFallback(); return; }
    const wb = XLSX.utils.book_new();
    // ----- Recursos -----
    const resRows = [["Equipo", "Jefe de Proyecto", "Recurso", "Rol"]];
    CL.TEAMS.forEach((tm) => tm.resources.forEach((r) => resRows.push([tm.team, tm.pm, r.name, r.role])));
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(resRows), "Recursos");
    // ----- Marcas (vista consolidada) -----
    const mk = [["Código", "N°", "Nombre de la solicitud", "Tipo de marca", "Jefe de Proyecto", "Equipo", "Recurso", "Rol", "Estado", "Fecha Inicio", "Fecha Fin", "División", "Departamento", "Color", "Días laborables"]];
    events.slice().sort((a, b) => (a.start < b.start ? -1 : 1)).forEach((e) => {
      const r = CL.RESOURCE_MAP.get(e.resourceId);
      const tm = r ? CL.TEAMS.find((x) => x.id === r.pmId) : null;
      const proj = e.projectId ? CL.PROJECT_MAP.get(e.projectId) : null;
      mk.push([
        e.projectId || "",
        proj && proj.numero ? proj.numero : "",
        proj ? proj.name : (e.title || ""),
        CL.EVENT_TYPES[e.type] ? CL.EVENT_TYPES[e.type].label : e.type,
        tm ? tm.pm : "",
        tm ? tm.team : "",
        r ? r.name : e.resourceId,
        r ? r.role : "",
        e.estado ? estadoLabel(e.estado) : "",
        e.start,
        e.end,
        proj ? (proj.division || "") : "",
        proj ? (proj.departamento || "") : "",
        proj ? proj.color : (CL.EVENT_TYPES[e.type] ? CL.EVENT_TYPES[e.type].color : ""),
        CL.workingDaysBetween(e.start, e.end),
      ]);
    });
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(mk), "Marcas");
    // ----- Solicitudes (catálogo) -----
    const sol = [["Código", "N°", "Nombre", "División", "Departamento", "Color"]];
    CL.PROJECTS.forEach((p) => sol.push([p.id, p.numero || "", p.name, p.division || "", p.departamento || "", p.color || ""]));
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(sol), "Solicitudes");
    // ----- KPIs -----
    const kp = [["Equipo", "Jefe de Proyecto", "% Cumplimiento", "% Ocupación", "Solicitudes", "En riesgo", "Prom. días/solicitud"]];
    CL.TEAMS.forEach((tm) => { const k = CLA.computeKpis(events, { type: "team", id: tm.id }, year); kp.push([tm.team, tm.pm, k.cumplimiento, k.ocupacion, k.atendidas, k.enRiesgo, k.promDias]); });
    const kg = CLA.computeKpis(events, { type: "all" }, year); kp.push(["TODOS", "", kg.cumplimiento, kg.ocupacion, kg.atendidas, kg.enRiesgo, kg.promDias]);
    XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(kp), "KPIs");
    XLSX.writeFile(wb, "carga-laboral-" + year + ".xlsx");
  }
  function exportCsvFallback() {
    const rows = [["Recurso", "Equipo", "Tipo", "Solicitud", "Detalle", "Inicio", "Fin", "Estado", "Días laborables"]];
    events.forEach((e) => { const r = CL.RESOURCE_MAP.get(e.resourceId); const tm = r ? CL.TEAMS.find((x) => x.id === r.pmId) : null; rows.push([r ? r.name : e.resourceId, tm ? tm.team : "", CL.EVENT_TYPES[e.type].label, e.projectId || "", e.title, e.start, e.end, e.estado ? estadoLabel(e.estado) : "", CL.workingDaysBetween(e.start, e.end)]); });
    const csv = rows.map((r) => r.map((c) => '"' + String(c).replace(/"/g, '""') + '"').join(",")).join("\n");
    downloadBlob(new Blob(["\ufeff" + csv], { type: "text/csv;charset=utf-8" }), "carga-laboral.csv");
  }
  function exportConfig() {
    setMenuOpen(false);
    const data = { app: "carga-laboral", version: 1, year: CL.YEAR, teams: CL.TEAMS, projects: CL.PROJECTS, events };
    downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }), "carga-laboral-respaldo.json");
  }
  function triggerImport() { setMenuOpen(false); if (fileRef.current) fileRef.current.click(); }
  function onImportFile(e) {
    const f = e.target.files && e.target.files[0]; if (!f) return;
    const reader = new FileReader();
    reader.onload = () => {
      try {
        const d = JSON.parse(reader.result);
        if (!Array.isArray(d.teams) || !Array.isArray(d.projects)) throw 0;
        CL.TEAMS = d.teams;
        CL.PROJECTS = d.projects.map((p) => Object.assign({ numero: 0, division: "", departamento: "" }, p));
        CL.rebuildIndexes(); CL.saveConfig();
        setEvents(Array.isArray(d.events) ? d.events.filter((ev) => CL.RESOURCE_MAP.has(ev.resourceId)) : []);
        setScope(defaultScope()); setOpenTeams(new Set([CL.TEAMS[0] ? CL.TEAMS[0].id : ""])); bump();
        showToast("Respaldo importado");
      } catch (err) { alert("No se pudo leer el archivo. Debe ser un respaldo .json válido de Carga Laboral."); }
    };
    reader.readAsText(f); e.target.value = "";
  }
  function askReset() { setMenuOpen(false); setModal({ kind: "confirm-reset" }); }
  function doReset() {
    try {
      localStorage.setItem("cl_config_v2", JSON.stringify({ teams: [], projects: [] }));
      localStorage.setItem(STORE_KEY, "[]");
    } catch (e) {}
    location.reload();
  }

  // ----- Importar desde Excel (solicitudes + marcas por nombre de recurso) -----
  function triggerExcel() { setMenuOpen(false); if (excelRef.current) excelRef.current.click(); }
  function triggerTeamsExcel() { setMenuOpen(false); if (teamsExcelRef.current) teamsExcelRef.current.click(); }
  function onTeamsExcelFile(e) {
    const f = e.target.files && e.target.files[0]; if (!f) return;
    const reader = new FileReader();
    reader.onload = () => {
      try {
        if (typeof XLSX === "undefined") throw new Error("La librería Excel no está cargada.");
        const wb = XLSX.read(reader.result, { type: "array", cellDates: true });
        const res = importTeamsFromWorkbook(wb);
        if (res.addedTeams || res.addedResources) { CL.saveConfig(); bump(); }
        const partes = [];
        if (res.addedTeams) partes.push(res.addedTeams + " Jefe(s) de Proyecto / equipo(s)");
        if (res.addedResources) partes.push(res.addedResources + " recurso(s)");
        if (res.skipped) partes.push(res.skipped + " fila(s) omitida(s)");
        showToast(partes.length ? "Importado: " + partes.join(" · ") : "Sin cambios desde el archivo");
        if (res.errors.length) {
          const head = "Carga finalizada con observaciones:\n\n";
          const list = res.errors.slice(0, 15).join("\n");
          const tail = res.errors.length > 15 ? "\n…(" + (res.errors.length - 15) + " observación(es) más)" : "";
          alert(head + list + tail);
        }
      } catch (err) {
        alert("No se pudo leer el archivo Excel: " + (err && err.message ? err.message : "formato no válido") + "\n\nFormato esperado: una hoja con columnas \"Jefe de Proyecto\", \"Equipo\", \"Recurso\" (opcional), \"Rol\" (opcional).");
      }
    };
    reader.readAsArrayBuffer(f); e.target.value = "";
  }
  function onExcelFile(e) {
    const f = e.target.files && e.target.files[0]; if (!f) { return; }
    const reader = new FileReader();
    reader.onload = () => {
      try {
        if (typeof XLSX === "undefined") throw new Error("La librería Excel no está cargada.");
        const wb = XLSX.read(reader.result, { type: "array", cellDates: true });
        const res = importFromWorkbook(wb);
        const updatedCount = res.updatedProjectIds ? res.updatedProjectIds.length : 0;
        if (res.newProjects.length) CL.PROJECTS = CL.PROJECTS.concat(res.newProjects);
        if (res.newProjects.length || updatedCount) { CL.rebuildIndexes(); CL.saveConfig(); }
        if (res.addedMarcas.length) setEvents((prev) => prev.concat(res.addedMarcas));
        bump();
        const partes = [];
        if (res.addedMarcas.length) partes.push(res.addedMarcas.length + " marca" + (res.addedMarcas.length === 1 ? "" : "s"));
        if (res.newProjects.length) partes.push(res.newProjects.length + " solicitud" + (res.newProjects.length === 1 ? "" : "es") + " nueva" + (res.newProjects.length === 1 ? "" : "s"));
        if (updatedCount) partes.push(updatedCount + " solicitud" + (updatedCount === 1 ? "" : "es") + " actualizada" + (updatedCount === 1 ? "" : "s"));
        if (res.skipped) partes.push(res.skipped + " omitida" + (res.skipped === 1 ? "" : "s"));
        showToast(partes.length ? "Importado: " + partes.join(" · ") : "Sin cambios desde el archivo");
        if (res.errors.length) {
          const head = "Importación finalizada con observaciones:\n\n";
          const list = res.errors.slice(0, 15).join("\n");
          const tail = res.errors.length > 15 ? "\n…(" + (res.errors.length - 15) + " observación(es) más)" : "";
          alert(head + list + tail);
        }
      } catch (err) {
        alert("No se pudo leer el archivo Excel: " + (err && err.message ? err.message : "formato no válido"));
      }
    };
    reader.readAsArrayBuffer(f); e.target.value = "";
  }

  const tabs = [
    { k: "calendario", label: "Calendario", icon: Icon.cal },
    { k: "anual", label: "Resumen anual", icon: Icon.grid },
    { k: "kpis", label: "KPIs", icon: Icon.chart },
  ];

  return (
    <div className={"app" + (sidebarCollapsed ? " app-collapsed" : "")}>
      <Sidebar scope={scope} setScope={setScope} openTeams={openTeams} toggleTeam={toggleTeam} year={year}
        collapsed={sidebarCollapsed} onToggleCollapse={toggleSidebar}
        onNewPM={() => setModal({ kind: "pm", initial: null })}
        onEditPM={(tm) => setModal({ kind: "pm", initial: tm })}
        onDeletePM={deletePM}
        onNewResource={(tm) => setModal({ kind: "resource", initial: null, teamId: tm.id, teamName: tm.team })}
        onEditResource={(r, tm) => setModal({ kind: "resource", initial: r, teamId: tm.id, teamName: tm.team })}
        onDeleteResource={deleteResource} />

      <div className="main">
        <div className="topbar">
          <div className="crumb">
            <h1>{scopeName}</h1>
            <div className="sub">{scopeSub}</div>
          </div>
          <div className="tabs">
            {tabs.map((tb) => (
              <button key={tb.k} className={"tab" + (tab === tb.k ? " active" : "")} onClick={() => setTab(tb.k)}>{tb.icon({})}{tb.label}</button>
            ))}
          </div>
          <div className="spacer"></div>
          <div className="alert-wrap">
            <button className={"btn btn-ghost alert-btn" + (alerts.length > 0 ? " has-alerts" : "")} onClick={() => setAlertOpen((o) => !o)} title={alerts.length > 0 ? alerts.length + " alerta(s) del día" : "Sin alertas del día"}>
              {Icon.bell({ width: 16, height: 16 })}
              {alerts.length > 0 && <span className="alert-badge">{alerts.length}</span>}
            </button>
            {alertOpen && (
              <div className="alert-panel">
                <div className="alert-head">
                  <div>
                    <div className="alert-title">Alertas del día</div>
                    <div className="alert-sub">{alerts.length === 0 ? "No hay alertas hoy" : alerts.length + " marca" + (alerts.length === 1 ? "" : "s") + " pasaron a En Riesgo"}</div>
                  </div>
                  {alerts.length > 0 && <button className="navbtn" title="Limpiar lista de hoy" onClick={() => { setAlerts([]); try { localStorage.removeItem(ALERTS_KEY); } catch(e){} }}>{Icon.x({ width: 14, height: 14 })}</button>}
                </div>
                {alerts.length === 0 ? (
                  <div className="alert-empty">Las asignaciones que vencen en ≤ 3 días hábiles pasarán automáticamente a En Riesgo y aparecerán aquí.</div>
                ) : (
                  <div className="alert-list">
                    {alerts.slice().sort((a, b) => a.diasRestantes - b.diasRestantes).map((a) => (
                      <button key={a.id} className="alert-item" onClick={() => { pickResource(a.resourceId); setAlertOpen(false); }}>
                        <span className="alert-dot" style={{ background: a.color }}></span>
                        <div className="alert-body">
                          <div className="alert-row1">{a.projectId ? a.projectId + " · " : ""}{a.projectName}</div>
                          <div className="alert-row2">{a.resourceName}{a.teamName ? " · " + a.teamName : ""}</div>
                          <div className="alert-row3">
                            <span className={"chip " + (a.diasRestantes === 0 ? "bad" : "warn")}>
                              {a.diasRestantes === 0 ? "Vence hoy" : a.diasRestantes === 1 ? "1 día hábil restante" : a.diasRestantes + " días hábiles restantes"}
                            </span>
                            <span className="alert-meta">vence {a.end} · {a.at}</span>
                          </div>
                        </div>
                        {Icon.chevR({ width: 13, height: 13 })}
                      </button>
                    ))}
                  </div>
                )}
              </div>
            )}
          </div>
          <button className="btn btn-ghost" onClick={() => setModal({ kind: "solicitudes-view" })} title="Vista consolidada de todas las marcas / solicitudes">{Icon.doc({ width: 15, height: 15 })} Solicitudes</button>
          <div className="data-menu-wrap">
            <button className="btn btn-ghost" onClick={() => setMenuOpen((o) => !o)} title="Exportar, importar y restablecer">{Icon.download({ width: 15, height: 15 })} Datos {Icon.chevDown({ width: 13, height: 13 })}</button>
            {menuOpen && (
              <div className="data-menu">
                <div className="menu-cap">Importar</div>
                <button className="menu-item" onClick={triggerTeamsExcel}>{Icon.users({})}<span>Cargar Jefes de Proyecto + Recursos<div className="mi-sub">Desde Excel: PM, Equipo, Recurso, Rol</div></span></button>
                <button className="menu-item" onClick={triggerExcel}>{Icon.upload({})}<span>Importar desde Excel<div className="mi-sub">Solicitudes y marcas por recurso</div></span></button>
                <button className="menu-item" onClick={triggerImport}>{Icon.download({})}<span>Importar respaldo (.json)<div className="mi-sub">Cargar un .json exportado</div></span></button>
                <div className="menu-sep"></div>
                <div className="menu-cap">Exportar</div>
                <button className="menu-item" onClick={exportExcel}>{Icon.sheet({})}<span>Exportar a Excel<div className="mi-sub">Recursos, marcas y KPIs</div></span></button>
                <button className="menu-item" onClick={exportConfig}>{Icon.download({})}<span>Exportar respaldo<div className="mi-sub">Archivo .json para restaurar</div></span></button>
                <div className="menu-sep"></div>
                <button className="menu-item danger" onClick={askReset}>{Icon.refresh({})}<span>Restablecer<div className="mi-sub">Borra todos los datos y deja la app vacía</div></span></button>
              </div>
            )}
            <input ref={fileRef} type="file" accept="application/json,.json" style={{ display: "none" }} onChange={onImportFile} />
            <input ref={excelRef} type="file" accept=".xlsx,.xls,.csv" style={{ display: "none" }} onChange={onExcelFile} />
            <input ref={teamsExcelRef} type="file" accept=".xlsx,.xls,.csv" style={{ display: "none" }} onChange={onTeamsExcelFile} />
          </div>
          <div className="year-select">
            <button className="navbtn" onClick={() => changeYear(year - 1)} disabled={year <= CL.MIN_YEAR} title="Año anterior">{Icon.chevL({})}</button>
            <span className="ys-val">{Icon.cal({ width: 14, height: 14 })}{year}</span>
            <button className="navbtn" onClick={() => changeYear(year + 1)} disabled={year >= CL.MAX_YEAR} title="Año siguiente">{Icon.chevR({})}</button>
          </div>
        </div>

        <div className="content">
          {tab === "calendario" && (
            scope.type === "resource" && resource ? (
              <div className="cal-layout">
                <Calendar scope={scope} resource={resource} resourceIds={resourceIds} events={events} year={year} month={month} setMonth={setMonth} onPrev={prevMonth} onNext={nextMonth} canPrev={canPrev} canNext={canNext} onAddOpen={openAdd} onEditOpen={openEdit} setTip={setTip} />
                <ResourceSidePanel resource={resource} events={events} year={year} month={month} />
              </div>
            ) : scope.type === "all" ? (
              <TeamGroupedCalendar
                teams={CL.TEAMS}
                events={events}
                year={year}
                month={month}
                onPrev={prevMonth}
                onNext={nextMonth}
                canPrev={canPrev}
                canNext={canNext}
                expandedTeams={expandedCentralTeams}
                onToggleTeam={(id) => setExpandedCentralTeams((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; })}
                onExpandAll={() => setExpandedCentralTeams(new Set(CL.TEAMS.map((t) => t.id)))}
                onCollapseAll={() => setExpandedCentralTeams(new Set())}
                onSelectTeam={(id) => { setScope({ type: "team", id }); setOpenTeams((p) => new Set(p).add(id)); }}
                onPickResource={pickResource}
                setTip={setTip}
              />
            ) : (
              <Calendar scope={scope} resource={null} resourceIds={resourceIds} events={events} year={year} month={month} setMonth={setMonth} onPrev={prevMonth} onNext={nextMonth} canPrev={canPrev} canNext={canNext} onAddOpen={openAdd} onEditOpen={openEdit} setTip={setTip} />
            )
          )}
          {tab === "anual" && <AnnualHeatmap events={events} scope={scope} year={year} onPickResource={pickResource} setTip={setTip} />}
          {tab === "kpis" && <KpisView events={events} scope={scope} year={year} />}
        </div>
      </div>

      {modal?.kind === "event" && (modal.resource || resource) && <EventModal initial={modal.initial} resource={modal.resource || resource} onSave={saveEvent} onDelete={deleteEvent} onClose={close} />}
      {modal?.kind === "pm" && <PMModal initial={modal.initial} onSave={savePM} onDelete={(id) => { const tm = CL.TEAMS.find((x) => x.id === id); if (tm) deletePM(tm); }} onClose={close} />}
      {modal?.kind === "resource" && <ResourceModal initial={modal.initial} teamName={modal.teamName} onSave={(d) => saveResource(d, modal.teamId)} onDelete={(id) => { const tm = CL.TEAMS.find((x) => x.id === modal.teamId); const r = tm?.resources.find((x) => x.id === id); if (r && tm) deleteResource(r, tm); }} onClose={close} />}
      {modal?.kind === "projects" && <ProjectsModal events={events} onCommit={saveProjects} onClose={close} />}
      {modal?.kind === "solicitudes-view" && (
        <SolicitudesView
          events={events}
          onEditEvent={(ev) => { const r = CL.RESOURCE_MAP.get(ev.resourceId); if (r) setModal({ kind: "event", initial: ev, resource: r, returnTo: "solicitudes-view" }); }}
          onAskDeleteEvent={(ev) => setModal({ kind: "confirm-delete-event", event: ev, returnTo: "solicitudes-view" })}
          onAskDeleteAll={() => setModal({ kind: "confirm-delete-all", returnTo: "solicitudes-view" })}
          onManageCatalog={() => setModal({ kind: "projects", returnTo: "solicitudes-view" })}
          onClose={close}
        />
      )}
      {modal?.kind === "confirm-delete-event" && (
        <ConfirmModal
          title="Eliminar marca"
          danger
          message={`¿Eliminar la marca "${modal.event.title || modal.event.projectId || CL.EVENT_TYPES[modal.event.type]?.label}" de ${CL.RESOURCE_MAP.get(modal.event.resourceId)?.name || "este recurso"}?`}
          note={`${modal.event.start} → ${modal.event.end}. Esta acción no se puede deshacer.`}
          confirmLabel="Eliminar marca"
          cancelLabel="Cancelar"
          onConfirm={() => deleteEvent(modal.event.id)}
          onClose={close}
        />
      )}
      {modal?.kind === "confirm-delete-all" && (
        <ConfirmModal
          title="Eliminar todas las solicitudes"
          danger
          message={`Esto borrará ${events.length} marca${events.length === 1 ? "" : "s"} del calendario (asignaciones, vacaciones, licencias y capacitaciones).`}
          note="Los códigos de solicitud del catálogo NO se eliminan — solo las marcas (eventos) asociadas a recursos. Esta acción no se puede deshacer."
          confirmLabel={`Sí, borrar las ${events.length}`}
          cancelLabel="Cancelar"
          onConfirm={() => { setEvents([]); showToast(`${events.length} marca${events.length === 1 ? "" : "s"} eliminada${events.length === 1 ? "" : "s"}`); close(); }}
          onClose={close}
        />
      )}
      {modal?.kind === "confirm-reset" && (
        <ConfirmModal
          title="Restablecer la aplicación"
          danger
          message="Esto borrará todos los equipos, recursos, solicitudes y marcas que tienes registrados. La aplicación quedará vacía."
          note="Esta acción no se puede deshacer. Si quieres conservar tu información, primero exporta un respaldo desde el menú Datos."
          confirmLabel="Sí, borrar todo"
          cancelLabel="Cancelar"
          onConfirm={doReset}
          onClose={close}
        />
      )}

      <Tooltip tip={tip} />
      {toast && <div className="toast">{Icon.check({ width: 16, height: 16 })}{toast}</div>}

      <TweaksPanel>
        <TweakSection label="Estilo visual" />
        <TweakRadio label="Acento" value={t.tema} options={["balanceado", "rojo", "morado"]} onChange={(v) => setTweak("tema", v)} />
        <TweakToggle label="Degradados vivos" value={t.vivo} onChange={(v) => setTweak("vivo", v)} />
      </TweaksPanel>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
