diff --git a/frontend/src/components/ConfigModal.jsx b/frontend/src/components/ConfigModal.jsx index be4c256..d379cea 100644 --- a/frontend/src/components/ConfigModal.jsx +++ b/frontend/src/components/ConfigModal.jsx @@ -1,6 +1,7 @@ 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 }) { @@ -875,6 +876,15 @@ export default function ConfigModal({ onClose }) { background: seccion === "leyendas" ? "#e2e8f0" : "transparent", color: "#1e293b" }}> Leyenda + {/* Separador sutil: lo de abajo es de otra naturaleza (acciones) */} +
+
- {seccion === "plantillas" ? ( + {seccion === "subir_base" ? ( +
+ +
+ ) : seccion === "plantillas" ? (
@@ -980,3 +995,211 @@ export default function ConfigModal({ onClose }) { ); } + +// ── 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 ( +
+ + {abierto && ( +
+ {opciones.length === 0 &&
Cargando etiquetas…
} + {opciones.map((t) => { + const on = seleccion.includes(t); + return ( + + ); + })} +
+ )} +
+ ); +} + +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) => {n}; + + return ( +
+
+ Sube un Excel con una columna TELEFONO (con código de país, ej. 51966319975). + 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. Si el número no existe, se borra automáticamente (contacto y conversación). +
+ +
+ + ({ value: String(a.id), label: a.nombre }))]} + onChange={(v) => setAsesorSel(v)} + minWidth={200} /> + {asesorSel === "SIN" && ( +
+ No se asignará ningún asesor. Si un número ya tenía asesor, se mantiene (no se quita). +
+ )} +
+ +
+ + + {archivo && ✓ {archivo} — {telefonos.length} número(s)} +
+ + + + {resultado && ( +
+ {resultado.error ? resultado.error : ( +
+ ✅ Proceso completado
+ Enviados correctamente: {(resultado.enviados || []).length}
+ Borrados (número no existe): {(resultado.borrados || []).length}
+ {(resultado.invalidos || []).length > 0 && <>Formato inválido: {resultado.invalidos.length}
} + {(resultado.fallo || []).length > 0 && <>No se pudieron crear: {resultado.fallo.length}} +
+ )} +
+ )} + + {confirmSin && ( +
setConfirmSin(false)} style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,.5)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1300, padding: 16 }}> +
e.stopPropagation()} style={{ background: "#fff", borderRadius: 14, maxWidth: 400, width: "100%", padding: 22, boxShadow: "0 20px 60px rgba(0,0,0,.3)" }}> +
Sin asignación
+
+ Elegiste SIN ASIGNACIÓN. Se crearán los contactos y se pondrán las etiquetas, pero + no se asignará ningún asesor. Si un número ya tenía asesor, se mantiene tal cual. ¿Continuar? +
+
+ + +
+
+
+ )} +
+ ); +}