Files
dashboard-leads/frontend/src/components/ConfigModal.jsx

983 lines
53 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect, useRef } from "react";
import { api } from "../lib/api";
import Plantillas from "../pages/Plantillas";
// Desplegable con búsqueda (estilo Claude).
function ProgramaSelect({ valor, opciones = [], onSelect }) {
const [abierto, setAbierto] = useState(false);
const [q, setQ] = useState("");
const ref = useRef(null);
useEffect(() => {
function fuera(e) { if (ref.current && !ref.current.contains(e.target)) setAbierto(false); }
document.addEventListener("mousedown", fuera);
return () => document.removeEventListener("mousedown", fuera);
}, []);
const filtradas = opciones.filter((o) => o.label.toLowerCase().includes(q.toLowerCase()));
return (
<div ref={ref} style={{ position: "relative", width: "100%" }}>
<button onClick={() => setAbierto((v) => !v)}
style={{ width: "100%", textAlign: "left", padding: "7px 10px", border: "1px solid #cbd5e1",
borderRadius: 8, fontSize: 13, background: "#fff", cursor: "pointer",
color: valor ? "#334155" : "#94a3b8", fontFamily: "inherit",
display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{valor || "Seleccionar programa..."}
</span>
<span style={{ color: "#94a3b8", marginLeft: 6 }}>⌄</span>
</button>
{abierto && (
<div style={{ position: "absolute", top: "calc(100% + 6px)", left: 0, right: 0, background: "#fff",
border: "1px solid #e2e8f0", borderRadius: 12, boxShadow: "0 12px 34px rgba(0,0,0,0.18)",
zIndex: 100, overflow: "hidden" }}>
<div style={{ padding: 10, borderBottom: "1px solid #f1f5f9" }}>
<input autoFocus value={q} onChange={(e) => setQ(e.target.value)} placeholder="Buscar..."
style={{ width: "100%", padding: "8px 11px", border: "1px solid #cbd5e1", borderRadius: 8,
fontSize: 13, fontFamily: "inherit", outline: "none" }} />
</div>
<div style={{ maxHeight: 220, overflow: "auto" }}>
{filtradas.length === 0 ? (
<div style={{ padding: "12px 14px", fontSize: 13, color: "#94a3b8" }}>Sin opciones</div>
) : filtradas.map((o) => (
<div key={o.num_indice} style={{ padding: "9px 14px", fontSize: 13, color: "#334155", cursor: "pointer" }}
onMouseEnter={(e) => e.currentTarget.style.background = "#f1f5f9"}
onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}
onMouseDown={(e) => e.preventDefault()}
onClick={() => { onSelect && onSelect(o); setAbierto(false); setQ(""); }}>
{o.label}
</div>
))}
</div>
</div>
)}
</div>
);
}
// Apartado "Sedes de Anuncios": conjuntos del CSV sin sede -> asignar sede manual.
// FASE 1: solo vista (dropdown de sede por fila). El guardar se agrega despues.
function SedesAnuncios() {
const [filas, setFilas] = useState([]);
const [cargando, setCargando] = useState(true);
const [sel, setSel] = useState({}); // conjunto -> sede elegida (decorativo por ahora)
const [selProg, setSelProg] = useState({}); // conjunto -> programa elegido (decorativo)
const [busca, setBusca] = useState("");
// Opciones fijas (no se pueden borrar) y opciones ampliables con "Agregar".
const SEDES_FIJAS = ["LIMA", "PIURA", "AREQUIPA", "TRUJILLO"];
const PROGS_FIJOS = ["TEAC", "TERC", "SEMINARIOS", "OTROS"];
const [sedes, setSedes] = useState(SEDES_FIJAS);
const [programas, setProgramas] = useState(PROGS_FIJOS);
const SEDES = sedes;
const PROGRAMAS = programas;
// Quita una opción creada (no fija): la saca del dropdown y de cualquier
// conjunto que la tenga asignada (se aplica al Guardar).
function quitarOpcion(tipo, val) {
if (tipo === "TIPO") {
if (SEDES_FIJAS.includes(val)) return;
setSedes((s) => s.filter((x) => x !== val));
setSel((cur) => {
const n = { ...cur };
Object.keys(n).forEach((k) => { if (n[k] === val) n[k] = ""; });
return n;
});
} else {
if (PROGS_FIJOS.includes(val)) return;
setProgramas((p) => p.filter((x) => x !== val));
setSelProg((cur) => {
const n = { ...cur };
Object.keys(n).forEach((k) => { if (n[k] === val) n[k] = ""; });
return n;
});
}
}
// Pop-up "Agregar"
const [popup, setPopup] = useState(false);
const [nvTipo, setNvTipo] = useState("TIPO"); // "TIPO" o "PROGRAMA"
const [nvValor, setNvValor] = useState("");
function agregar() {
const val = nvValor.trim().toUpperCase();
if (!val) return;
if (nvTipo === "TIPO") {
if (!sedes.includes(val)) setSedes((s) => [...s, val]);
} else {
if (!programas.includes(val)) setProgramas((p) => [...p, val]);
}
setNvValor(""); setPopup(false);
}
const [guardando, setGuardando] = useState(false);
const [msg, setMsg] = useState("");
useEffect(() => {
api.conjuntosSinSede()
.then((r) => {
const rows = r || [];
setFilas(rows);
// precargar sede/programa ya asignados + ampliar opciones si hay valores nuevos
const s0 = {}, p0 = {}; const sExtra = new Set(), pExtra = new Set();
rows.forEach((f) => {
if (f.sede) { s0[f.conjunto] = f.sede; sExtra.add(f.sede); }
if (f.programa) { p0[f.conjunto] = f.programa; pExtra.add(f.programa); }
});
setSel(s0); setSelProg(p0);
setSedes((cur) => [...cur, ...[...sExtra].filter((x) => !cur.includes(x))]);
// Carga los programas ya asignados (incluye los nuevos que crees, ej.
// BRANDING). El dashboard ahora SÍ reconoce cualquier programa asignado.
setProgramas((cur) => [...cur, ...[...pExtra].filter((x) => !cur.includes(x))]);
setCargando(false);
})
.catch(() => setCargando(false));
}, []);
async function guardar() {
setGuardando(true); setMsg("");
try {
const cambios = filas
.filter((f) => sel[f.conjunto] || selProg[f.conjunto])
.map((f) => ({ conjunto: f.conjunto, sede: sel[f.conjunto] || "", programa: selProg[f.conjunto] || "" }));
await api.guardarConjuntoSede(cambios);
setMsg("✓ Guardado. Se reflejará en ROAS.");
window.dispatchEvent(new Event("roas-actualizar")); // avisa al ROAS que recargue
window.dispatchEvent(new Event("datos-actualizar")); // avisa a TODOS los apartados
} catch (e) {
setMsg("Error al guardar");
}
setGuardando(false);
}
async function borrar(conjunto) {
// limpia la seleccion en pantalla y borra la asignacion en Supabase
setSel((s) => { const n = { ...s }; delete n[conjunto]; return n; });
setSelProg((s) => { const n = { ...s }; delete n[conjunto]; return n; });
try {
await api.borrarConjuntoSede(conjunto);
setMsg("✓ Asignación borrada.");
window.dispatchEvent(new Event("roas-actualizar")); // avisa al ROAS que recargue
window.dispatchEvent(new Event("datos-actualizar")); // avisa a TODOS los apartados
} catch (e) {
setMsg("Error al borrar");
}
}
const vis = filas.filter((f) => f.conjunto.toLowerCase().includes(busca.toLowerCase()));
return (
<>
<div style={{ padding: "14px 22px 8px" }}>
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
<input value={busca} onChange={(e) => setBusca(e.target.value)}
placeholder="Buscar conjunto de anuncio..."
style={{ flex: 1, padding: "9px 12px", border: "1px solid #cbd5e1",
borderRadius: 9, fontSize: 13 }} />
<button onClick={() => setPopup(true)}
style={{ padding: "9px 16px", border: "none", background: "#1e3a5f", color: "#fff",
borderRadius: 9, fontSize: 13, fontWeight: 700, cursor: "pointer", whiteSpace: "nowrap" }}>
+ Agregar
</button>
</div>
<div style={{ fontSize: 12, color: "#64748b", marginTop: 8 }}>
Conjuntos de anuncios sin sede asignada. Elige la sede y el programa de cada uno.
</div>
</div>
{/* Pop-up Agregar nueva opción (Tipo o Programa) */}
{popup && (
<div onClick={() => setPopup(false)}
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,0.4)", zIndex: 60,
display: "flex", alignItems: "center", justifyContent: "center" }}>
<div onClick={(e) => e.stopPropagation()}
style={{ background: "#fff", borderRadius: 14, width: 360, padding: 22,
boxShadow: "0 20px 50px rgba(0,0,0,0.3)" }}>
<div style={{ fontSize: 16, fontWeight: 700, color: "#0f172a", marginBottom: 16 }}>
Agregar nueva opción
</div>
<label style={{ fontSize: 12, fontWeight: 600, color: "#64748b" }}>Clasificación</label>
<select value={nvTipo} onChange={(e) => setNvTipo(e.target.value)}
style={{ width: "100%", padding: "9px 10px", border: "1px solid #cbd5e1",
borderRadius: 8, fontSize: 14, marginTop: 5, marginBottom: 14 }}>
<option value="TIPO">Tipo (Sede)</option>
<option value="PROGRAMA">Programa</option>
</select>
<label style={{ fontSize: 12, fontWeight: 600, color: "#64748b" }}>Nombre</label>
<input value={nvValor} onChange={(e) => setNvValor(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && agregar()}
placeholder="Ej: CHICLAYO"
style={{ width: "100%", padding: "9px 10px", border: "1px solid #cbd5e1",
borderRadius: 8, fontSize: 14, marginTop: 5, marginBottom: 14 }} />
{/* Lista de opciones CREADAS (no fijas) para poder borrarlas */}
{(() => {
const esSede = nvTipo === "TIPO";
const lista = (esSede ? sedes : programas)
.filter((x) => !(esSede ? SEDES_FIJAS : PROGS_FIJOS).includes(x));
if (lista.length === 0) return null;
return (
<div style={{ marginBottom: 16 }}>
<div style={{ fontSize: 12, fontWeight: 600, color: "#64748b", marginBottom: 6 }}>
{esSede ? "Sedes creadas" : "Programas creados"} (clic en × para borrar)
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
{lista.map((x) => (
<span key={x} style={{ display: "inline-flex", alignItems: "center", gap: 6,
background: "#f1f5f9", border: "1px solid #e2e8f0",
borderRadius: 999, padding: "4px 6px 4px 11px", fontSize: 12.5,
fontWeight: 600, color: "#334155" }}>
{x}
<button onClick={() => quitarOpcion(nvTipo, x)} title="Borrar"
style={{ width: 18, height: 18, borderRadius: "50%", border: "none",
background: "#fee2e2", color: "#ef4444", cursor: "pointer",
fontSize: 13, lineHeight: 1, display: "inline-flex",
alignItems: "center", justifyContent: "center" }}>×</button>
</span>
))}
</div>
</div>
);
})()}
<div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
<button onClick={() => setPopup(false)}
style={{ padding: "8px 16px", border: "1px solid #cbd5e1", background: "#fff",
color: "#64748b", borderRadius: 8, fontSize: 13, fontWeight: 600, cursor: "pointer" }}>
Cancelar
</button>
<button onClick={agregar}
style={{ padding: "8px 18px", border: "none", background: "#1e3a5f", color: "#fff",
borderRadius: 8, fontSize: 13, fontWeight: 700, cursor: "pointer" }}>
Guardar
</button>
</div>
</div>
</div>
)}
<div style={{ flex: 1, overflow: "hidden", padding: "0 22px 4px" }}>
{cargando ? (
<div style={{ padding: 40, textAlign: "center", color: "#94a3b8" }}>Cargando...</div>
) : (
<div style={{ height: "100%", border: "1px solid #e2e8f0", borderRadius: 12, overflow: "auto" }}>
<table style={{ width: "100%", borderCollapse: "separate", borderSpacing: 0, tableLayout: "fixed" }}>
<thead>
<tr>
<th style={{ textAlign: "center", padding: "11px 14px", position: "sticky", top: 0,
background: "#1e3a5f", color: "#fff", fontSize: 12.5, fontWeight: 700,
borderRight: "1px solid rgba(255,255,255,0.12)" }}>Conjunto de Anuncio</th>
<th style={{ textAlign: "center", padding: "11px 14px", position: "sticky", top: 0,
background: "#1e3a5f", color: "#fff", fontSize: 12.5, fontWeight: 700, width: 150,
borderRight: "1px solid rgba(255,255,255,0.12)" }}>Tipo</th>
<th style={{ textAlign: "center", padding: "11px 14px", position: "sticky", top: 0,
background: "#1e3a5f", color: "#fff", fontSize: 12.5, fontWeight: 700, width: 150,
borderRight: "1px solid rgba(255,255,255,0.12)" }}>Programa</th>
<th style={{ padding: "11px 8px", position: "sticky", top: 0,
background: "#1e3a5f", color: "#fff", fontSize: 12.5, fontWeight: 700, width: 48 }}></th>
</tr>
</thead>
<tbody>
{vis.map((f, i) => (
<tr key={f.conjunto} style={{ background: i % 2 ? "#f8fafc" : "#fff" }}>
<td style={{ padding: "10px 14px", fontSize: 13, color: "#1e293b",
whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis",
borderRight: "1px solid #eef2f7" }}>{f.conjunto}</td>
<td style={{ padding: "10px 14px", borderRight: "1px solid #eef2f7" }}>
<select value={sel[f.conjunto] || ""}
onChange={(e) => setSel((s) => ({ ...s, [f.conjunto]: e.target.value }))}
style={{ width: "100%", padding: "6px 8px", border: "1px solid #cbd5e1",
borderRadius: 7, fontSize: 13 }}>
<option value="">— Sin asignar —</option>
{SEDES.map((s) => <option key={s} value={s}>{s}</option>)}
</select>
</td>
<td style={{ padding: "10px 14px", borderRight: "1px solid #eef2f7" }}>
<select value={selProg[f.conjunto] || ""}
onChange={(e) => setSelProg((s) => ({ ...s, [f.conjunto]: e.target.value }))}
style={{ width: "100%", padding: "6px 8px", border: "1px solid #cbd5e1",
borderRadius: 7, fontSize: 13 }}>
<option value="">— Sin asignar —</option>
{PROGRAMAS.map((p) => <option key={p} value={p}>{p}</option>)}
</select>
</td>
<td style={{ padding: "10px 8px", textAlign: "center" }}>
{(sel[f.conjunto] || selProg[f.conjunto]) && (
<button onClick={() => borrar(f.conjunto)} title="Borrar asignación"
style={{ border: "none", background: "transparent", color: "#dc2626",
cursor: "pointer", fontSize: 16, fontWeight: 700, lineHeight: 1 }}>
×
</button>
)}
</td>
</tr>
))}
{vis.length === 0 && (
<tr><td colSpan={4} style={{ padding: 30, textAlign: "center", color: "#94a3b8", fontSize: 13 }}>
No hay conjuntos sin sede.</td></tr>
)}
</tbody>
</table>
</div>
)}
</div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center",
padding: "14px 22px", borderTop: "1px solid #e2e8f0" }}>
<span style={{ fontSize: 13, color: msg.startsWith("✓") ? "#047857" : (msg ? "#b91c1c" : "#94a3b8") }}>
{msg || `${vis.length} conjuntos sin sede`}
</span>
<button onClick={guardar} disabled={guardando}
style={{ padding: "9px 22px", border: "none", background: "#1e3a5f", color: "#fff",
borderRadius: 9, fontSize: 14, fontWeight: 700, cursor: guardando ? "wait" : "pointer" }}>
{guardando ? "Guardando..." : "Guardar"}
</button>
</div>
</>
);
}
// Apartado "Programas": lista de programas con contar=NO (ocultos), con switch para reactivar.
function ProgramasOcultos() {
const [filas, setFilas] = useState([]);
const [cargando, setCargando] = useState(true);
const [enc, setEnc] = useState({}); // num_indice -> true si se va a encender
const [guardando, setGuardando] = useState(false);
const [msg, setMsg] = useState("");
useEffect(() => {
api.programasOcultos()
.then((r) => { setFilas(r.filas || []); setCargando(false); })
.catch(() => setCargando(false));
}, []);
async function guardar() {
const nis = Object.entries(enc).filter(([, v]) => v).map(([ni]) => ni);
if (nis.length === 0) { setMsg("No hay programas para activar."); return; }
setGuardando(true); setMsg("");
try {
await api.encenderProgramas(nis);
setMsg("✓ Activados. Ya aparecen en la matriz.");
const r = await api.programasOcultos();
setFilas(r.filas || []); setEnc({});
window.dispatchEvent(new Event("datos-actualizar")); // avisa a TODOS los apartados
} catch (e) { setMsg("Error: " + e.message); }
finally { setGuardando(false); }
}
return (
<>
<div style={{ padding: "16px 22px 6px" }}>
<div style={{ fontSize: 13, color: "#64748b", lineHeight: 1.5 }}>
Aquí están los programas ocultos del <b>Detalle por Curso</b>. Enciende el interruptor
y guarda para que vuelvan a aparecer en la tabla del dashboard.
</div>
</div>
<div style={{ flex: 1, overflow: "auto", padding: "8px 22px" }}>
{cargando ? (
<div style={{ padding: 40, textAlign: "center", color: "#94a3b8" }}>Cargando...</div>
) : filas.length === 0 ? (
<div style={{ padding: 30, textAlign: "center", color: "#94a3b8", fontSize: 13 }}>
No hay programas ocultos.
</div>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{filas.map((f) => {
const on = !!enc[f.num_indice];
return (
<div key={f.num_indice} style={{ display: "flex", alignItems: "center", justifyContent: "space-between",
padding: "12px 14px", border: "1px solid #e2e8f0", borderRadius: 10, background: "#fff" }}>
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: 13.5, fontWeight: 600, color: "#0f172a" }}>{f.dsc_det_programa}</div>
<div style={{ fontSize: 12, color: "#94a3b8" }}>
Índice {f.num_indice}{f.fch_inicio ? ` · ${f.fch_inicio}` : ""}{f.pauta ? ` · Pauta ${f.pauta}` : ""}
</div>
</div>
<div onClick={() => setEnc((p) => ({ ...p, [f.num_indice]: !p[f.num_indice] }))}
style={{ width: 46, height: 26, borderRadius: 999, cursor: "pointer", flexShrink: 0,
background: on ? "#2563eb" : "#cbd5e1", position: "relative", transition: "background .15s" }}>
<div style={{ width: 20, height: 20, borderRadius: "50%", background: "#fff",
position: "absolute", top: 3, left: on ? 23 : 3, transition: "left .15s",
boxShadow: "0 1px 3px rgba(0,0,0,0.3)" }} />
</div>
</div>
);
})}
</div>
)}
</div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center",
padding: "14px 22px", borderTop: "1px solid #e2e8f0" }}>
<span style={{ fontSize: 13, color: msg.startsWith("✓") ? "#047857" : "#b91c1c" }}>{msg}</span>
<button onClick={guardar} disabled={guardando}
style={{ padding: "9px 22px", border: "none", background: "#1e3a5f", color: "#fff",
borderRadius: 9, fontSize: 14, fontWeight: 700, cursor: guardando ? "wait" : "pointer" }}>
{guardando ? "Guardando..." : "Guardar"}
</button>
</div>
</>
);
}
// ── Mini-apartado LEYENDA: tabla de campanias + Agregar (popup) + eliminar (visual) ──
function LeyendaCampanias() {
const CARGOS = ["TEAC", "TERC", "VRF", "CO2", "Seminarios", "Diseño de Chillers", "Diplomado REF", "CAD", "Amoniaco", "Cámaras", "Diseño de Sistemas", "Metrado y Costeo"];
const SEDES = ["Lima", "Piura", "Arequipa", "Trujillo"];
const ORIGENES = ["Pauta_wsp", "Web_Whatsapp"];
const DIAS = ["Domingo", "Sábado", "Semipresencial", "-"];
const [filas, setFilas] = useState([]);
const [cargando, setCargando] = useState(true);
const [busca, setBusca] = useState("");
const [popup, setPopup] = useState(false);
const [editandoId, setEditandoId] = useState(null); // id de la fila en edicion, o null = agregar
const [confirmar, setConfirmar] = useState(null); // fila a eliminar, o null
const [choques, setChoques] = useState(null); // [filas que chocan] o null (popup de aviso)
const [guardando, setGuardando] = useState(false);
const [msg, setMsg] = useState("");
const nuevaVacia = { frase_busqueda: "", cargo: "TEAC", codigo: "", sede: "Lima", dia: "-", origen: "Pauta_wsp", fecha_inicio: "", fecha_fin: "" };
const [nueva, setNueva] = useState(nuevaVacia);
function recargar() {
setCargando(true);
api.campanias().then((r) => { setFilas(r.filas || []); setCargando(false); })
.catch(() => setCargando(false));
}
useEffect(() => { recargar(); }, []);
const vis = filas.filter((f) =>
!busca || ((f.frase_busqueda || "") + (f.cargo || "") + (f.codigo || "") + (f.sede || "")).toLowerCase().includes(busca.toLowerCase()));
// Extrae los emojis de un texto (pictográficos). Devuelve un Set de emojis.
function emojisDe(txt) {
const s = new Set();
for (const ch of String(txt || "")) {
const cp = ch.codePointAt(0);
// rangos de emojis/pictogramas comunes
if ((cp >= 0x1F000 && cp <= 0x1FAFF) || (cp >= 0x2600 && cp <= 0x27BF) ||
(cp >= 0x2B00 && cp <= 0x2BFF) || cp === 0x2764 || (cp >= 0x2190 && cp <= 0x21FF)) {
s.add(ch);
}
}
return s;
}
// ¿los rangos [i1,f1] y [i2,f2] se solapan? (fechas en texto YYYY-MM-DD)
function fechasSolapan(i1, f1, i2, f2) {
if (!i1 || !f1 || !i2 || !f2) return false;
return i1 <= f2 && i2 <= f1;
}
// Devuelve las campañas existentes que chocan con "nueva":
// mismo emoji compartido Y fechas solapadas.
function detectarChoques() {
const emN = emojisDe(nueva.frase_busqueda);
if (emN.size === 0) return [];
const iN = (nueva.fecha_inicio || "").slice(0, 10);
const fN = (nueva.fecha_fin || "").slice(0, 10);
return filas.filter((f) => {
if (editandoId != null && f.id === editandoId) return false; // no chocar contra si misma
const emE = emojisDe(f.frase_busqueda);
const comparte = [...emN].some((e) => emE.has(e)); // ¿comparten algún emoji?
if (!comparte) return false;
return fechasSolapan(iN, fN, (f.fecha_inicio || "").slice(0, 10), (f.fecha_fin || "").slice(0, 10));
});
}
function abrirAgregar() {
setEditandoId(null); setNueva(nuevaVacia); setMsg(""); setPopup(true);
}
function abrirEditar(f) {
setEditandoId(f.id);
setNueva({
frase_busqueda: f.frase_busqueda || "", cargo: f.cargo || "TEAC",
codigo: f.codigo || "", sede: f.sede || "Lima", dia: f.dia || "-",
origen: f.origen || "Pauta_wsp",
fecha_inicio: (f.fecha_inicio || "").slice(0, 10),
fecha_fin: (f.fecha_fin || "").slice(0, 10),
});
setMsg(""); setPopup(true);
}
async function agregar() {
if (!nueva.frase_busqueda.trim()) { setMsg("Escribe la frase de búsqueda."); return; }
// Aviso de choque: mismo emoji + fechas solapadas. Si las fechas NO chocan, no avisa.
const ch = detectarChoques();
if (ch.length > 0) { setChoques(ch); return; }
await guardarCampania();
}
async function guardarCampania() {
setChoques(null);
setGuardando(true); setMsg("");
try {
if (editandoId != null) await api.editarCampania(editandoId, nueva);
else await api.agregarCampania(nueva);
setPopup(false); setNueva(nuevaVacia); setEditandoId(null);
recargar();
window.dispatchEvent(new Event("datos-actualizar"));
} catch (e) { setMsg("Error al guardar: " + e.message); }
finally { setGuardando(false); }
}
async function borrarConfirmado() {
const f = confirmar;
setConfirmar(null);
try {
await api.borrarCampania(f.id);
recargar();
window.dispatchEvent(new Event("datos-actualizar"));
} catch (e) { setMsg("Error al borrar: " + e.message); }
}
const inp = { width: "100%", height: 38, border: "1px solid #e2e8f0", borderRadius: 8,
padding: "0 11px", fontSize: 13, boxSizing: "border-box" };
const lab = { fontSize: 12.5, fontWeight: 600, color: "#475569", display: "block", marginBottom: 5 };
return (
<>
<div style={{ padding: "14px 22px 8px", display: "flex", gap: 10 }}>
<input value={busca} onChange={(e) => setBusca(e.target.value)}
placeholder="Buscar frase, cargo, código o sede..."
style={{ flex: 1, padding: "9px 12px", border: "1px solid #cbd5e1", borderRadius: 9, fontSize: 13 }} />
<button onClick={abrirAgregar}
style={{ padding: "0 16px", background: "#1e3a8a", color: "#fff", border: "none",
borderRadius: 9, fontSize: 13, fontWeight: 700, cursor: "pointer", whiteSpace: "nowrap" }}>
+ Agregar leyenda
</button>
</div>
<div style={{ flex: 1, overflow: "hidden", padding: "0 22px 12px" }}>
<div style={{ height: "100%", border: "1px solid #e2e8f0", borderRadius: 12, overflow: "auto" }}>
<table style={{ minWidth: 920, width: "100%", borderCollapse: "separate", borderSpacing: 0, fontSize: 12.5 }}>
<thead>
<tr>
{["Frase de búsqueda", "Cargo", "Código", "Sede", "Día", "Origen", "Inicio", "Fin", ""].map((h, i, arr) => (
<th key={i} style={{ position: "sticky", top: 0, zIndex: 20, background: "#1e3a5f", color: "#fff",
padding: "10px 12px", fontWeight: 600, textAlign: "center", whiteSpace: "nowrap",
borderRight: i < arr.length - 1 ? "1px solid rgba(255,255,255,0.12)" : "none" }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{cargando && (
<tr><td colSpan={9} style={{ padding: 40, textAlign: "center", color: "#94a3b8" }}>Cargando...</td></tr>
)}
{!cargando && vis.length === 0 && (
<tr><td colSpan={9} style={{ padding: 40, textAlign: "center", color: "#94a3b8" }}>Sin leyendas.</td></tr>
)}
{!cargando && vis.map((f) => (
<tr key={f.id}>
<td style={{ padding: "9px 12px", minWidth: 220, textAlign: "left", borderRight: "1px solid #eef2f7" }}>{f.frase_busqueda}</td>
<td style={{ padding: "9px 12px", whiteSpace: "nowrap", textAlign: "center", borderRight: "1px solid #eef2f7" }}>{f.cargo}</td>
<td style={{ padding: "9px 12px", fontWeight: 600, whiteSpace: "nowrap", textAlign: "center", borderRight: "1px solid #eef2f7" }}>{f.codigo}</td>
<td style={{ padding: "9px 12px", whiteSpace: "nowrap", textAlign: "center", borderRight: "1px solid #eef2f7" }}>{f.sede}</td>
<td style={{ padding: "9px 12px", whiteSpace: "nowrap", textAlign: "center", borderRight: "1px solid #eef2f7" }}>{f.dia}</td>
<td style={{ padding: "9px 12px", whiteSpace: "nowrap", textAlign: "center", borderRight: "1px solid #eef2f7" }}>{f.origen}</td>
<td style={{ padding: "9px 12px", whiteSpace: "nowrap", textAlign: "center", borderRight: "1px solid #eef2f7" }}>{f.fecha_inicio}</td>
<td style={{ padding: "9px 12px", whiteSpace: "nowrap", textAlign: "center", borderRight: "1px solid #eef2f7" }}>{f.fecha_fin}</td>
<td style={{ padding: "9px 12px", textAlign: "center", whiteSpace: "nowrap" }}>
<div style={{ display: "inline-flex", gap: 6, alignItems: "center" }}>
<button onClick={() => abrirEditar(f)} title="Editar leyenda"
onMouseEnter={(e) => { e.currentTarget.style.background = "#2563eb"; e.currentTarget.style.color = "#fff"; e.currentTarget.style.borderColor = "#2563eb"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "#eff6ff"; e.currentTarget.style.color = "#2563eb"; e.currentTarget.style.borderColor = "#bfdbfe"; }}
style={{ width: 28, height: 28, borderRadius: 8, background: "#eff6ff",
border: "1px solid #bfdbfe", color: "#2563eb", cursor: "pointer",
fontSize: 14, lineHeight: 1, display: "inline-flex",
alignItems: "center", justifyContent: "center",
transition: "all 0.15s ease" }}>✎</button>
<button onClick={() => setConfirmar(f)} title="Eliminar leyenda"
onMouseEnter={(e) => { e.currentTarget.style.background = "#ef4444"; e.currentTarget.style.color = "#fff"; e.currentTarget.style.borderColor = "#ef4444"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "#fef2f2"; e.currentTarget.style.color = "#ef4444"; e.currentTarget.style.borderColor = "#fecaca"; }}
style={{ width: 28, height: 28, borderRadius: 8, background: "#fef2f2",
border: "1px solid #fecaca", color: "#ef4444", cursor: "pointer",
fontSize: 16, fontWeight: 700, lineHeight: 1, display: "inline-flex",
alignItems: "center", justifyContent: "center",
transition: "all 0.15s ease" }}>×</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Popup Agregar leyenda */}
{popup && (
<div onClick={() => setPopup(false)}
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,0.45)",
display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1300 }}>
<div onClick={(e) => e.stopPropagation()}
style={{ background: "#fff", borderRadius: 14, width: 520, maxWidth: "94vw",
boxShadow: "0 20px 50px rgba(0,0,0,.25)", overflow: "hidden" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center",
padding: "16px 20px", borderBottom: "1px solid #eef2f7" }}>
<div style={{ fontSize: 16, fontWeight: 700, color: "#0f172a" }}>{editandoId != null ? "Editar leyenda" : "Agregar leyenda"}</div>
<button onClick={() => setPopup(false)}
style={{ background: "transparent", border: "none", fontSize: 20, color: "#94a3b8", cursor: "pointer" }}>×</button>
</div>
<div style={{ padding: "18px 20px", display: "flex", flexDirection: "column", gap: 14 }}>
<div>
<label style={lab}>Frase de búsqueda</label>
<input style={inp} value={nueva.frase_busqueda} placeholder="Ej. 🖥️ Estoy interesado en sus Seminarios"
onChange={(e) => setNueva({ ...nueva, frase_busqueda: e.target.value })} />
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<div>
<label style={lab}>Cargo / programa</label>
<select style={{ ...inp, padding: "0 8px", background: "#fff" }} value={nueva.cargo}
onChange={(e) => setNueva({ ...nueva, cargo: e.target.value })}>
{CARGOS.map((c) => <option key={c}>{c}</option>)}
</select>
</div>
<div>
<label style={lab}>Código</label>
<input style={inp} value={nueva.codigo} placeholder="Ej. 74a"
onChange={(e) => setNueva({ ...nueva, codigo: e.target.value })} />
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<div>
<label style={lab}>Sede</label>
<select style={{ ...inp, padding: "0 8px", background: "#fff" }} value={nueva.sede}
onChange={(e) => setNueva({ ...nueva, sede: e.target.value })}>
{SEDES.map((s) => <option key={s}>{s}</option>)}
</select>
</div>
<div>
<label style={lab}>Origen</label>
<select style={{ ...inp, padding: "0 8px", background: "#fff" }} value={nueva.origen}
onChange={(e) => setNueva({ ...nueva, origen: e.target.value })}>
{ORIGENES.map((o) => <option key={o}>{o}</option>)}
</select>
</div>
</div>
<div>
<label style={lab}>Día / frecuencia</label>
<select style={{ ...inp, padding: "0 8px", background: "#fff" }} value={nueva.dia}
onChange={(e) => setNueva({ ...nueva, dia: e.target.value })}>
{DIAS.map((d) => <option key={d}>{d}</option>)}
</select>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
<div>
<label style={lab}>Fecha inicio</label>
<input type="date" style={inp} value={nueva.fecha_inicio}
onChange={(e) => setNueva({ ...nueva, fecha_inicio: e.target.value })} />
</div>
<div>
<label style={lab}>Fecha fin</label>
<input type="date" style={inp} value={nueva.fecha_fin}
onChange={(e) => setNueva({ ...nueva, fecha_fin: e.target.value })} />
</div>
</div>
</div>
<div style={{ display: "flex", justifyContent: "flex-end", gap: 10, padding: "14px 20px",
borderTop: "1px solid #eef2f7", background: "#f8fafc" }}>
<button onClick={() => setPopup(false)}
style={{ height: 38, padding: "0 16px", border: "1px solid #cbd5e1", background: "#fff",
borderRadius: 8, fontSize: 13, fontWeight: 600, color: "#475569", cursor: "pointer" }}>Cancelar</button>
<button onClick={agregar} disabled={guardando}
style={{ height: 38, padding: "0 18px", border: "none", background: "#1e3a8a",
borderRadius: 8, fontSize: 13, fontWeight: 700, color: "#fff",
cursor: guardando ? "default" : "pointer", opacity: guardando ? 0.6 : 1 }}>
{guardando ? "Guardando..." : (editandoId != null ? "Guardar cambios" : "Guardar leyenda")}
</button>
</div>
{msg && <div style={{ padding: "0 20px 14px", fontSize: 12.5,
color: msg.startsWith("Error") ? "#ef4444" : "#0f766e" }}>{msg}</div>}
</div>
</div>
)}
{/* Popup de confirmación de eliminación */}
{confirmar !== null && (
<div onClick={() => setConfirmar(null)}
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,0.45)",
display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1400 }}>
<div onClick={(e) => e.stopPropagation()}
style={{ background: "#fff", borderRadius: 14, width: 400, maxWidth: "92vw",
boxShadow: "0 20px 50px rgba(0,0,0,.25)", overflow: "hidden", textAlign: "center" }}>
<div style={{ padding: "26px 24px 8px" }}>
<div style={{ width: 52, height: 52, borderRadius: "50%", background: "#fef2f2",
display: "flex", alignItems: "center", justifyContent: "center",
margin: "0 auto 14px", fontSize: 26, color: "#ef4444" }}>🗑️</div>
<div style={{ fontSize: 16, fontWeight: 700, color: "#0f172a", marginBottom: 6 }}>
¿Eliminar esta leyenda?
</div>
<div style={{ fontSize: 13, color: "#64748b", lineHeight: 1.5 }}>
{confirmar && (
<>Se eliminará la pauta <b style={{ color: "#0f172a" }}>{confirmar.codigo}</b>
{" "}({confirmar.cargo} — {confirmar.sede}). Esta acción no se puede deshacer.</>
)}
</div>
</div>
<div style={{ display: "flex", gap: 10, padding: "18px 24px 22px" }}>
<button onClick={() => setConfirmar(null)}
style={{ flex: 1, height: 40, border: "1px solid #cbd5e1", background: "#fff",
borderRadius: 9, fontSize: 13.5, fontWeight: 600, color: "#475569", cursor: "pointer" }}>Cancelar</button>
<button onClick={borrarConfirmado}
style={{ flex: 1, height: 40, border: "none", background: "#ef4444",
borderRadius: 9, fontSize: 13.5, fontWeight: 700, color: "#fff", cursor: "pointer" }}>Sí, eliminar</button>
</div>
</div>
</div>
)}
{/* Popup de ADVERTENCIA de choque: mismo emoji + fechas solapadas */}
{choques !== null && (
<div onClick={() => setChoques(null)}
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,0.45)",
display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1500 }}>
<div onClick={(e) => e.stopPropagation()}
style={{ background: "#fff", borderRadius: 14, width: 460, maxWidth: "94vw",
boxShadow: "0 20px 50px rgba(0,0,0,.25)", overflow: "hidden" }}>
<div style={{ padding: "24px 24px 6px", textAlign: "center" }}>
<div style={{ width: 52, height: 52, borderRadius: "50%", background: "#fff7ed",
display: "flex", alignItems: "center", justifyContent: "center",
margin: "0 auto 14px", fontSize: 26 }}>⚠️</div>
<div style={{ fontSize: 16, fontWeight: 700, color: "#0f172a", marginBottom: 6 }}>
Esta leyenda podría duplicar leads
</div>
<div style={{ fontSize: 13, color: "#64748b", lineHeight: 1.5 }}>
La frase que vas a crear comparte emoji y fechas con {choques.length === 1 ? "esta pauta existente" : `estas ${choques.length} pautas existentes`}.
Un mismo mensaje podría contarse dos veces.
</div>
</div>
<div style={{ padding: "12px 20px", display: "flex", flexDirection: "column", gap: 8, maxHeight: 240, overflowY: "auto" }}>
{choques.map((c) => (
<div key={c.id} style={{ border: "1px solid #fed7aa", background: "#fffbeb",
borderRadius: 10, padding: "10px 12px" }}>
<div style={{ fontSize: 13, fontWeight: 700, color: "#0f172a", marginBottom: 3 }}>
Pauta {c.codigo} <span style={{ fontWeight: 500, color: "#92400e" }}>({c.cargo} — {c.sede})</span>
</div>
<div style={{ fontSize: 12.5, color: "#475569", marginBottom: 4 }}>“{c.frase_busqueda}”</div>
<div style={{ fontSize: 12, color: "#b45309", fontWeight: 600 }}>
Vigente: {c.fecha_inicio} → {c.fecha_fin}
</div>
</div>
))}
</div>
<div style={{ display: "flex", gap: 10, padding: "12px 24px 20px" }}>
<button onClick={() => setChoques(null)}
style={{ flex: 1, height: 40, border: "1px solid #cbd5e1", background: "#fff",
borderRadius: 9, fontSize: 13.5, fontWeight: 600, color: "#475569", cursor: "pointer" }}>Cancelar</button>
<button onClick={guardarCampania} disabled={guardando}
style={{ flex: 1, height: 40, border: "none", background: "#ea580c",
borderRadius: 9, fontSize: 13.5, fontWeight: 700, color: "#fff",
cursor: guardando ? "default" : "pointer", opacity: guardando ? 0.6 : 1 }}>
{guardando ? "Guardando..." : "Crear de todos modos"}
</button>
</div>
</div>
</div>
)}
</>
);
}
export default function ConfigModal({ onClose }) {
const [seccion, setSeccion] = useState("leyenda");
const [filas, setFilas] = useState([]);
const [cargando, setCargando] = useState(true);
const [guardando, setGuardando] = useState(false);
const [msg, setMsg] = useState("");
const [busca, setBusca] = useState("");
const [edit, setEdit] = useState({}); // conjunto -> pauta editada
const [editProg, setEditProg] = useState({}); // conjunto -> {num_indice, label} programa elegido
const [progOpc, setProgOpc] = useState([]); // opciones del desplegable de Programa
useEffect(() => {
api.leyendaAnuncios()
.then((r) => { setFilas(r.filas || []); setCargando(false); })
.catch(() => setCargando(false));
api.programasDisponibles()
.then((r) => setProgOpc(r.programas || []))
.catch(() => setProgOpc([]));
}, []);
const setPauta = (conjunto, val) => setEdit((p) => ({ ...p, [conjunto]: val }));
const valorPauta = (f) => (edit[f.conjunto] !== undefined ? edit[f.conjunto] : (f.pauta || ""));
// Programa mostrado: el elegido si hay, si no el que ya trae la fila
const valorPrograma = (f) => (editProg[f.conjunto] !== undefined ? editProg[f.conjunto].label : (f.programa || ""));
async function guardar() {
// Un cambio por conjunto que tenga pauta editada o programa elegido
const conjuntos = new Set([...Object.keys(edit), ...Object.keys(editProg)]);
const cambios = [];
for (const conjunto of conjuntos) {
const f = filas.find((x) => x.conjunto === conjunto) || {};
const pauta = (edit[conjunto] !== undefined ? edit[conjunto] : (f.pauta || "")).trim();
const prog = editProg[conjunto]; // {num_indice, label} o undefined
cambios.push({ conjunto, pauta, num_indice: prog ? prog.num_indice : null });
}
if (cambios.length === 0) { setMsg("No hay cambios."); return; }
setGuardando(true); setMsg("");
try {
await api.guardarLeyenda(cambios);
setMsg("✓ Guardado correctamente.");
const r = await api.leyendaAnuncios();
setFilas(r.filas || []); setEdit({}); setEditProg({});
window.dispatchEvent(new Event("datos-actualizar")); // avisa a TODOS los apartados
} catch (e) { setMsg("Error: " + e.message); }
finally { setGuardando(false); }
}
// Orden: 1) pauta Y programa, 2) pauta sin programa, 3) programa sin pauta, 4) nada
const _rango = (f) => {
const p = valorPauta(f).trim() !== "";
const g = (f.programa || "").trim() !== "";
if (p && g) return 0;
if (p && !g) return 1;
if (!p && g) return 2;
return 3;
};
const filtradas = filas
.filter((f) =>
!busca || f.conjunto.toLowerCase().includes(busca.toLowerCase())
|| String(f.pauta).includes(busca)
|| (f.programa || "").toLowerCase().includes(busca.toLowerCase()))
.sort((a, b) => (_rango(a) - _rango(b)) || a.conjunto.localeCompare(b.conjunto));
return (
<div onClick={onClose}
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,0.55)",
display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1200 }}>
<div onClick={(e) => e.stopPropagation()}
style={{ background: "#fff", borderRadius: 16, width: 1320, maxWidth: "96vw",
height: 680, maxHeight: "92vh", display: "flex", overflow: "hidden",
boxShadow: "0 24px 70px rgba(0,0,0,0.4)",
fontFamily: "system-ui, -apple-system, 'Segoe UI', sans-serif" }}>
{/* Sidebar del modal */}
<div style={{ width: 240, flexShrink: 0, background: "#f8fafc", borderRight: "1px solid #e2e8f0", padding: "18px 12px" }}>
<div style={{ fontSize: 12, fontWeight: 700, color: "#94a3b8", textTransform: "uppercase",
padding: "0 10px 10px" }}>Configuración</div>
<button onClick={() => setSeccion("leyenda")}
style={{ width: "100%", textAlign: "left", padding: "10px 12px", borderRadius: 8,
border: "none", cursor: "pointer", fontSize: 14, fontWeight: 600, marginBottom: 4,
whiteSpace: "nowrap",
background: seccion === "leyenda" ? "#e2e8f0" : "transparent", color: "#1e293b" }}>
Leyenda de Anuncios
</button>
<button onClick={() => setSeccion("programas")}
style={{ width: "100%", textAlign: "left", padding: "10px 12px", borderRadius: 8,
border: "none", cursor: "pointer", fontSize: 14, fontWeight: 600, marginBottom: 4,
whiteSpace: "nowrap",
background: seccion === "programas" ? "#e2e8f0" : "transparent", color: "#1e293b" }}>
Programas
</button>
<button onClick={() => setSeccion("sedes")}
style={{ width: "100%", textAlign: "left", padding: "10px 12px", borderRadius: 8,
border: "none", cursor: "pointer", fontSize: 14, fontWeight: 600, marginBottom: 4,
whiteSpace: "nowrap",
background: seccion === "sedes" ? "#e2e8f0" : "transparent", color: "#1e293b" }}>
Sedes de Anuncios
</button>
<button onClick={() => setSeccion("leyendas")}
style={{ width: "100%", textAlign: "left", padding: "10px 12px", borderRadius: 8,
border: "none", cursor: "pointer", fontSize: 14, fontWeight: 600, whiteSpace: "nowrap",
marginBottom: 4,
background: seccion === "leyendas" ? "#e2e8f0" : "transparent", color: "#1e293b" }}>
Leyenda
</button>
<button onClick={() => setSeccion("plantillas")}
style={{ width: "100%", textAlign: "left", padding: "10px 12px", borderRadius: 8,
border: "none", cursor: "pointer", fontSize: 14, fontWeight: 600, whiteSpace: "nowrap",
background: seccion === "plantillas" ? "#e2e8f0" : "transparent", color: "#1e293b" }}>
Plantillas
</button>
</div>
{/* Contenido */}
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center",
padding: "16px 22px", borderBottom: "1px solid #e2e8f0" }}>
<div style={{ fontSize: 17, fontWeight: 700, color: "#0f172a" }}>
{seccion === "leyenda" ? "Leyenda de Anuncios"
: seccion === "programas" ? "Programas"
: seccion === "leyendas" ? "Leyenda"
: seccion === "plantillas" ? "Plantillas"
: "Sedes de Anuncios"}
</div>
<button onClick={onClose}
style={{ background: "transparent", border: "none", fontSize: 24, cursor: "pointer", color: "#64748b" }}>×</button>
</div>
{seccion === "plantillas" ? (
<div style={{ flex: 1, overflow: "auto", padding: "16px 22px 24px" }}>
<Plantillas ocultarTitulo />
</div>
) : seccion === "leyendas" ? (
<LeyendaCampanias />
) : seccion === "sedes" ? (
<SedesAnuncios />
) : seccion === "programas" ? (
<ProgramasOcultos />
) : (
<>
<div style={{ padding: "14px 22px 8px" }}>
<input value={busca} onChange={(e) => setBusca(e.target.value)}
placeholder="Buscar conjunto, pauta o programa..."
style={{ width: "100%", padding: "9px 12px", border: "1px solid #cbd5e1",
borderRadius: 9, fontSize: 13 }} />
</div>
<div style={{ flex: 1, overflow: "hidden", padding: "0 22px 4px" }}>
{cargando ? (
<div style={{ padding: 40, textAlign: "center", color: "#94a3b8" }}>Cargando...</div>
) : (
<div style={{ height: "100%", border: "1px solid #e2e8f0", borderRadius: 12,
overflow: "auto" }}>
<table style={{ width: "100%", borderCollapse: "separate", borderSpacing: 0,
tableLayout: "fixed" }}>
<colgroup>
<col style={{ width: "38%" }} />
<col style={{ width: 100 }} />
<col style={{ width: "auto" }} />
</colgroup>
<thead>
<tr>
<th style={{ position: "sticky", top: 0, zIndex: 20, background: "#1e3a5f", color: "#fff",
padding: "11px 14px", fontSize: 13, fontWeight: 600, textAlign: "left",
borderTopLeftRadius: 12 }}>Conjunto de Anuncios</th>
<th style={{ position: "sticky", top: 0, zIndex: 20, background: "#1e3a5f", color: "#fff",
padding: "11px 14px", fontSize: 13, fontWeight: 600, width: 110, textAlign: "left" }}>Pauta</th>
<th style={{ position: "sticky", top: 0, zIndex: 20, background: "#1e3a5f", color: "#fff",
padding: "11px 14px", fontSize: 13, fontWeight: 600, textAlign: "left", width: 340,
borderTopRightRadius: 12 }}>Programa</th>
</tr>
</thead>
<tbody>
{filtradas.map((f) => (
<tr key={f.conjunto}>
<td style={{ padding: "8px 14px", fontSize: 13, color: "#334155", textAlign: "left",
borderRight: "1px solid #eef2f7", wordBreak: "break-word" }}>{f.conjunto}</td>
<td style={{ padding: "8px 14px", borderRight: "1px solid #eef2f7", textAlign: "center" }}>
<input value={valorPauta(f)} onChange={(e) => setPauta(f.conjunto, e.target.value)}
style={{ width: 90, padding: "6px 8px", border: "1px solid #cbd5e1",
borderRadius: 6, fontSize: 13, textAlign: "left", fontFamily: "inherit",
background: edit[f.conjunto] !== undefined ? "#fffbeb" : "#fff" }} />
</td>
<td style={{ padding: "8px 14px", maxWidth: 0, position: "relative" }}>
<ProgramaSelect valor={valorPrograma(f)} opciones={progOpc}
onSelect={(o) => setEditProg((p) => ({ ...p, [f.conjunto]: o }))} />
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center",
padding: "14px 22px", borderTop: "1px solid #e2e8f0" }}>
<span style={{ fontSize: 13, color: msg.startsWith("✓") ? "#047857" : "#b91c1c" }}>{msg}</span>
<button onClick={guardar} disabled={guardando}
style={{ padding: "9px 22px", border: "none", background: "#1e3a5f", color: "#fff",
borderRadius: 9, fontSize: 14, fontWeight: 700, cursor: guardando ? "wait" : "pointer" }}>
{guardando ? "Guardando..." : "Guardar"}
</button>
</div>
</>
)}
</div>
</div>
</div>
);
}