1241 lines
66 KiB
JavaScript
1241 lines
66 KiB
JavaScript
import { useState, useEffect, useRef } from "react";
|
||
import { api } from "../lib/api";
|
||
import Plantillas from "../pages/Plantillas";
|
||
import { Select } from "./UI";
|
||
|
||
// 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>
|
||
{/* Separador sutil: lo de abajo es de otra naturaleza (acciones) */}
|
||
<div style={{ height: 1, background: "#e2e8f0", opacity: 0.6, margin: "12px 10px" }} />
|
||
<button onClick={() => setSeccion("subir_base")}
|
||
style={{ width: "100%", textAlign: "left", padding: "10px 12px", borderRadius: 8,
|
||
border: "none", cursor: "pointer", fontSize: 14, fontWeight: 600, whiteSpace: "nowrap",
|
||
marginBottom: 4,
|
||
background: seccion === "subir_base" ? "#e2e8f0" : "transparent", color: "#1e293b" }}>
|
||
Subir Base
|
||
</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"
|
||
: seccion === "subir_base" ? "Subir Base"
|
||
: "Sedes de Anuncios"}
|
||
</div>
|
||
<button onClick={onClose}
|
||
style={{ background: "transparent", border: "none", fontSize: 24, cursor: "pointer", color: "#64748b" }}>×</button>
|
||
</div>
|
||
|
||
{seccion === "subir_base" ? (
|
||
<div style={{ flex: 1, overflow: "auto", padding: "16px 22px 24px" }}>
|
||
<SubirBase />
|
||
</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>
|
||
);
|
||
}
|
||
|
||
// ── SUBIR BASE ─────────────────────────────────────────────────────────────
|
||
// Multi-select desplegable con checkboxes (para las etiquetas de Chatwoot)
|
||
function MultiSelectEtiquetas({ opciones, seleccion, onToggle }) {
|
||
const [abierto, setAbierto] = useState(false);
|
||
const ref = useRef(null);
|
||
useEffect(() => {
|
||
const fuera = (e) => { if (ref.current && !ref.current.contains(e.target)) setAbierto(false); };
|
||
document.addEventListener("mousedown", fuera);
|
||
return () => document.removeEventListener("mousedown", fuera);
|
||
}, []);
|
||
const texto = seleccion.length === 0 ? "— Elegir etiquetas —"
|
||
: seleccion.length === 1 ? seleccion[0]
|
||
: `${seleccion.length} etiquetas`;
|
||
return (
|
||
<div ref={ref} style={{ position: "relative" }}>
|
||
<button type="button" onClick={() => setAbierto((v) => !v)}
|
||
style={{ height: 40, width: "100%", textAlign: "left", padding: "0 12px",
|
||
border: "1px solid #cbd5e1", borderRadius: 9, background: "#fff",
|
||
color: "#1e293b", fontWeight: 500, cursor: "pointer", fontSize: 14,
|
||
display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}
|
||
onMouseEnter={(e) => { e.currentTarget.style.borderColor = "#94a3b8"; }}
|
||
onMouseLeave={(e) => { e.currentTarget.style.borderColor = "#cbd5e1"; }}>
|
||
<span style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{texto}</span>
|
||
<span style={{ color: "#94a3b8", fontSize: 11 }}>▼</span>
|
||
</button>
|
||
{abierto && (
|
||
<div style={{ position: "absolute", top: "100%", left: 0, right: 0, zIndex: 50, marginTop: 4,
|
||
background: "#fff", border: "1px solid #e2e8f0", borderRadius: 9,
|
||
boxShadow: "0 8px 24px rgba(0,0,0,0.12)", padding: 6, maxHeight: 280, overflowY: "auto" }}>
|
||
{opciones.length === 0 && <div style={{ padding: 8, color: "#94a3b8", fontSize: 13 }}>Cargando etiquetas…</div>}
|
||
{opciones.map((t) => {
|
||
const on = seleccion.includes(t);
|
||
return (
|
||
<label key={t} style={{ display: "flex", alignItems: "center", gap: 9, padding: "8px 10px",
|
||
borderRadius: 6, cursor: "pointer", fontSize: 13,
|
||
background: on ? "#eff6ff" : "transparent", color: on ? "#1e3a5f" : "#334155", fontWeight: on ? 600 : 500 }}
|
||
onMouseEnter={(e) => { if (!on) e.currentTarget.style.background = "#f8fafc"; }}
|
||
onMouseLeave={(e) => { if (!on) e.currentTarget.style.background = "transparent"; }}>
|
||
<input type="checkbox" checked={on} onChange={() => onToggle(t)} />
|
||
{t}
|
||
</label>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SubirBase() {
|
||
const [plantillas, setPlantillas] = useState([]);
|
||
const [plantillaSel, setPlantillaSel] = useState(null);
|
||
const [etiquetas, setEtiquetas] = useState([]);
|
||
const [etiqSel, setEtiqSel] = useState([]);
|
||
const [asesores, setAsesores] = useState([]);
|
||
const [asesorSel, setAsesorSel] = useState("SIN");
|
||
const [telefonos, setTelefonos] = useState([]);
|
||
const [archivo, setArchivo] = useState("");
|
||
const [enviando, setEnviando] = useState(false);
|
||
const [resultado, setResultado] = useState(null);
|
||
const [confirmSin, setConfirmSin] = useState(false);
|
||
|
||
useEffect(() => {
|
||
api.plantillas().then((r) => setPlantillas((r && r.plantillas) || [])).catch(() => {});
|
||
api.subirBaseEtiquetas().then((r) => setEtiquetas(r.etiquetas || [])).catch(() => {});
|
||
api.subirBaseAsesores().then((r) => setAsesores(r.asesores || [])).catch(() => {});
|
||
}, []);
|
||
|
||
async function importarExcel(e) {
|
||
const file = e.target.files?.[0];
|
||
if (!file) return;
|
||
setResultado(null);
|
||
const XLSX = await import("xlsx");
|
||
const buf = await file.arrayBuffer();
|
||
const wb = XLSX.read(buf, { type: "array" });
|
||
const ws = wb.Sheets[wb.SheetNames[0]];
|
||
const filas = XLSX.utils.sheet_to_json(ws, { defval: "" });
|
||
const nums = [];
|
||
for (const fila of filas) {
|
||
let val = "";
|
||
for (const k of Object.keys(fila)) {
|
||
if (k.trim().toLowerCase() === "telefono") { val = fila[k]; break; }
|
||
}
|
||
const s = String(val).trim();
|
||
if (s) nums.push(s);
|
||
}
|
||
setTelefonos(nums);
|
||
setArchivo(file.name);
|
||
}
|
||
|
||
function toggleEtiqueta(t) {
|
||
setEtiqSel((p) => p.includes(t) ? p.filter((x) => x !== t) : [...p, t]);
|
||
}
|
||
|
||
async function enviar() {
|
||
if (enviando) return;
|
||
setEnviando(true); setResultado(null);
|
||
try {
|
||
const r = await api.subirBaseEnviar({
|
||
nombre: plantillaSel.nombre,
|
||
categoria: plantillaSel.categoria || "MARKETING",
|
||
idioma: "es_PE",
|
||
texto: plantillaSel.texto || "",
|
||
telefonos,
|
||
etiquetas: etiqSel,
|
||
asesor_id: asesorSel === "SIN" ? null : Number(asesorSel),
|
||
});
|
||
setResultado(r);
|
||
} catch (err) {
|
||
setResultado({ error: "Error: " + err.message });
|
||
}
|
||
setEnviando(false);
|
||
}
|
||
|
||
function intentarEnviar() {
|
||
if (!plantillaSel || telefonos.length === 0 || enviando) return;
|
||
if (asesorSel === "SIN") { setConfirmSin(true); return; }
|
||
enviar();
|
||
}
|
||
|
||
const puede = plantillaSel && telefonos.length > 0 && !enviando;
|
||
// Estilos iguales a Plantillas (cards radius 14, step cuadrado, labels mayúscula)
|
||
const card = { background: "#fff", border: "1px solid #e6eaf0", borderRadius: 14,
|
||
padding: 20, boxShadow: "0 1px 3px rgba(15,23,42,.04)" };
|
||
const cardHead = { display: "flex", alignItems: "center", gap: 10, marginBottom: 4 };
|
||
const step = { width: 26, height: 26, borderRadius: 8, background: "#1e3a5f", color: "#fff",
|
||
display: "flex", alignItems: "center", justifyContent: "center",
|
||
fontSize: 13, fontWeight: 800, flexShrink: 0 };
|
||
const cardTitle = { fontSize: 15, fontWeight: 700, color: "#0f172a" };
|
||
const cardSub = { fontSize: 12.5, color: "#94a3b8", margin: "2px 0 14px 36px" };
|
||
const fieldLabel = { fontSize: 12, fontWeight: 700, color: "#64748b", marginBottom: 7,
|
||
textTransform: "uppercase", letterSpacing: ".02em", display: "block" };
|
||
|
||
return (
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 16, maxWidth: 1080 }}>
|
||
|
||
{/* PASO 1 — Plantilla + Asesor */}
|
||
<div style={card}>
|
||
<div style={cardHead}>
|
||
<span style={step}>1</span>
|
||
<span style={cardTitle}>Elige la plantilla y el asesor</span>
|
||
</div>
|
||
<div style={cardSub}>La plantilla se enviará a los números; el asesor se asignará a cada uno.</div>
|
||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
|
||
<div>
|
||
<label style={fieldLabel}>📄 Plantilla</label>
|
||
<Select value={plantillaSel ? plantillaSel.nombre : ""}
|
||
options={[{ value: "", label: "— Elige una plantilla —" },
|
||
...plantillas.map((p) => ({ value: p.nombre, label: p.nombre }))]}
|
||
onChange={(v) => setPlantillaSel(plantillas.find((p) => p.nombre === v) || null)}
|
||
minWidth={0} />
|
||
</div>
|
||
<div>
|
||
<label style={fieldLabel}>👤 Asesor (solo uno)</label>
|
||
<Select value={asesorSel}
|
||
options={[{ value: "SIN", label: "— SIN ASIGNACIÓN —" },
|
||
...asesores.map((a) => ({ value: String(a.id), label: a.nombre }))]}
|
||
onChange={(v) => setAsesorSel(v)}
|
||
minWidth={0} />
|
||
</div>
|
||
</div>
|
||
{asesorSel === "SIN" && (
|
||
<div style={{ fontSize: 11.5, color: "#92400e", background: "#fffbeb", border: "1px solid #fde68a", borderRadius: 8, padding: "7px 10px", marginTop: 12 }}>
|
||
No se asignará ningún asesor. Si un número ya tenía asesor, se mantiene (no se quita).
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* PASO 2 — Etiquetas */}
|
||
<div style={card}>
|
||
<div style={cardHead}>
|
||
<span style={step}>2</span>
|
||
<span style={cardTitle}>Etiquetas</span>
|
||
<span style={{ marginLeft: "auto", fontSize: 12, color: "#94a3b8", fontWeight: 600 }}>
|
||
{etiqSel.length} seleccionada{etiqSel.length === 1 ? "" : "s"}
|
||
</span>
|
||
</div>
|
||
<div style={cardSub}>Se agregan a cada número (no borran las que ya tuviera). Puedes elegir varias.</div>
|
||
<label style={fieldLabel}>🏷️ Etiquetas de Chatwoot</label>
|
||
<MultiSelectEtiquetas opciones={etiquetas} seleccion={etiqSel} onToggle={toggleEtiqueta} />
|
||
</div>
|
||
|
||
{/* PASO 3 — Importar Excel */}
|
||
<div style={card}>
|
||
<div style={cardHead}>
|
||
<span style={step}>3</span>
|
||
<span style={cardTitle}>Importar base</span>
|
||
</div>
|
||
<div style={cardSub}>
|
||
Excel con una columna <b>TELEFONO</b> (con código de país, ej. 51966319975).
|
||
Si un número no existe, se borra automáticamente.
|
||
</div>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap" }}>
|
||
<label style={{ display: "inline-flex", alignItems: "center", gap: 8, cursor: "pointer",
|
||
padding: "10px 18px", border: "1px solid #cbd5e1", borderRadius: 9,
|
||
background: "#f8fafc", fontSize: 13.5, fontWeight: 600, color: "#334155" }}>
|
||
📁 Elegir archivo
|
||
<input type="file" accept=".xlsx,.xls" onChange={importarExcel} style={{ display: "none" }} />
|
||
</label>
|
||
{archivo && <span style={{ fontSize: 13, color: "#15803d", fontWeight: 600 }}>✓ {archivo} — {telefonos.length} número(s)</span>}
|
||
</div>
|
||
</div>
|
||
|
||
<button onClick={intentarEnviar} disabled={!puede}
|
||
style={{ width: "100%", padding: "13px", borderRadius: 11, border: "none",
|
||
background: puede ? "#1e3a5f" : "#cbd5e1", color: "#fff", fontWeight: 700, fontSize: 14.5,
|
||
cursor: puede ? "pointer" : "default" }}>
|
||
{enviando ? "Procesando… (esto puede tardar)" : "🚀 Subir base y enviar plantilla"}
|
||
</button>
|
||
|
||
{resultado && (
|
||
<div style={{ marginTop: 14, padding: 14, borderRadius: 10, fontSize: 13, background: resultado.error ? "#fee2e2" : "#f0fdf4", border: `1px solid ${resultado.error ? "#fca5a5" : "#bbf7d0"}`, color: "#334155" }}>
|
||
{resultado.error ? resultado.error : (
|
||
<div style={{ lineHeight: 1.7 }}>
|
||
<b>✅ Proceso completado</b><br/>
|
||
Enviados correctamente: <b>{(resultado.enviados || []).length}</b><br/>
|
||
Borrados (número no existe): <b>{(resultado.borrados || []).length}</b><br/>
|
||
{(resultado.invalidos || []).length > 0 && <>Formato inválido: <b>{resultado.invalidos.length}</b><br/></>}
|
||
{(resultado.fallo || []).length > 0 && <>No se pudieron crear: <b>{resultado.fallo.length}</b></>}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{confirmSin && (
|
||
<div onClick={() => setConfirmSin(false)} style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,.5)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1300, padding: 16 }}>
|
||
<div onClick={(e) => e.stopPropagation()} style={{ background: "#fff", borderRadius: 14, maxWidth: 400, width: "100%", padding: 22, boxShadow: "0 20px 60px rgba(0,0,0,.3)" }}>
|
||
<div style={{ fontSize: 15, fontWeight: 700, color: "#0f172a", marginBottom: 10 }}>Sin asignación</div>
|
||
<div style={{ fontSize: 13.5, color: "#475569", lineHeight: 1.6, marginBottom: 18 }}>
|
||
Elegiste <b>SIN ASIGNACIÓN</b>. Se crearán los contactos y se pondrán las etiquetas, pero
|
||
<b> no se asignará ningún asesor</b>. Si un número ya tenía asesor, se mantiene tal cual. ¿Continuar?
|
||
</div>
|
||
<div style={{ display: "flex", gap: 10, justifyContent: "flex-end" }}>
|
||
<button onClick={() => setConfirmSin(false)} style={{ padding: "9px 18px", borderRadius: 8, border: "1px solid #cbd5e1", background: "#fff", color: "#475569", fontWeight: 600, fontSize: 13, cursor: "pointer" }}>Cancelar</button>
|
||
<button onClick={() => { setConfirmSin(false); enviar(); }} style={{ padding: "9px 20px", borderRadius: 8, border: "none", background: "#1e3a5f", color: "#fff", fontWeight: 700, fontSize: 13, cursor: "pointer" }}>Sí, continuar</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|