Actualizar frontend/src/components/ConfigModal.jsx
This commit is contained in:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user