Actualizar frontend/src/components/ConfigModal.jsx

This commit is contained in:
2026-10-07 12:02:15 -05:00
parent 11671ab4aa
commit 5dc6950b76

View File

@@ -1,6 +1,7 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { api } from "../lib/api"; import { api } from "../lib/api";
import Plantillas from "../pages/Plantillas"; import Plantillas from "../pages/Plantillas";
import { Select } from "./UI";
// Desplegable con búsqueda (estilo Claude). // Desplegable con búsqueda (estilo Claude).
function ProgramaSelect({ valor, opciones = [], onSelect }) { function ProgramaSelect({ valor, opciones = [], onSelect }) {
@@ -875,6 +876,15 @@ export default function ConfigModal({ onClose }) {
background: seccion === "leyendas" ? "#e2e8f0" : "transparent", color: "#1e293b" }}> background: seccion === "leyendas" ? "#e2e8f0" : "transparent", color: "#1e293b" }}>
Leyenda Leyenda
</button> </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")} <button onClick={() => setSeccion("plantillas")}
style={{ width: "100%", textAlign: "left", padding: "10px 12px", borderRadius: 8, style={{ width: "100%", textAlign: "left", padding: "10px 12px", borderRadius: 8,
border: "none", cursor: "pointer", fontSize: 14, fontWeight: 600, whiteSpace: "nowrap", border: "none", cursor: "pointer", fontSize: 14, fontWeight: 600, whiteSpace: "nowrap",
@@ -891,13 +901,18 @@ export default function ConfigModal({ onClose }) {
: seccion === "programas" ? "Programas" : seccion === "programas" ? "Programas"
: seccion === "leyendas" ? "Leyenda" : seccion === "leyendas" ? "Leyenda"
: seccion === "plantillas" ? "Plantillas" : seccion === "plantillas" ? "Plantillas"
: seccion === "subir_base" ? "Subir Base"
: "Sedes de Anuncios"} : "Sedes de Anuncios"}
</div> </div>
<button onClick={onClose} <button onClick={onClose}
style={{ background: "transparent", border: "none", fontSize: 24, cursor: "pointer", color: "#64748b" }}>×</button> style={{ background: "transparent", border: "none", fontSize: 24, cursor: "pointer", color: "#64748b" }}>×</button>
</div> </div>
{seccion === "plantillas" ? ( {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" }}> <div style={{ flex: 1, overflow: "auto", padding: "16px 22px 24px" }}>
<Plantillas ocultarTitulo /> <Plantillas ocultarTitulo />
</div> </div>
@@ -980,3 +995,211 @@ export default function ConfigModal({ onClose }) {
</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;
const lbl = { fontSize: 12.5, fontWeight: 700, color: "#334155", marginBottom: 8, display: "block" };
const card = { background: "#fff", border: "1px solid #e6eaf0", borderRadius: 12, padding: 16, marginBottom: 14 };
const num = (n) => <span style={{ width: 22, height: 22, borderRadius: 6, background: "#1e3a5f", color: "#fff", display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: 12, fontWeight: 700, marginRight: 8 }}>{n}</span>;
return (
<div style={{ maxWidth: 680 }}>
<div style={{ fontSize: 13, color: "#64748b", marginBottom: 16, lineHeight: 1.6 }}>
Sube un Excel con una columna <b>TELEFONO</b> (con código de país, ej. <code>51966319975</code>).
A cada número se le crea el contacto, se le asignan las etiquetas y el asesor que elijas, y se le
envía la plantilla. <b>Si el número no existe</b>, se borra automáticamente (contacto y conversación).
</div>
<div style={card}>
<label style={lbl}>{num(1)} Plantilla a enviar</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={200} />
</div>
<div style={card}>
<label style={lbl}>{num(2)} Etiquetas (puedes elegir varias)</label>
<MultiSelectEtiquetas opciones={etiquetas} seleccion={etiqSel} onToggle={toggleEtiqueta} />
</div>
<div style={card}>
<label style={lbl}>{num(3)} Asignar a un 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={200} />
{asesorSel === "SIN" && (
<div style={{ fontSize: 11.5, color: "#92400e", background: "#fffbeb", border: "1px solid #fde68a", borderRadius: 8, padding: "7px 10px", marginTop: 8 }}>
No se asignará ningún asesor. Si un número ya tenía asesor, se mantiene (no se quita).
</div>
)}
</div>
<div style={card}>
<label style={lbl}>{num(4)} Importar Excel</label>
<label style={{ display: "inline-flex", alignItems: "center", gap: 8, cursor: "pointer", padding: "9px 16px", border: "1px solid #cbd5e1", borderRadius: 8, background: "#f8fafc", fontSize: 13, fontWeight: 600, color: "#334155" }}>
📁 Elegir archivo
<input type="file" accept=".xlsx,.xls" onChange={importarExcel} style={{ display: "none" }} />
</label>
{archivo && <span style={{ fontSize: 12.5, color: "#15803d", marginLeft: 12 }}>✓ {archivo} — {telefonos.length} número(s)</span>}
</div>
<button onClick={intentarEnviar} disabled={!puede}
style={{ width: "100%", padding: "12px", borderRadius: 10, border: "none", background: puede ? "#1e3a5f" : "#cbd5e1", color: "#fff", fontWeight: 700, fontSize: 14, 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>
);
}