import { useState, useEffect, useMemo, useRef } from "react"; import { api } from "../lib/api"; // Lista desplegable con casillas para marcar varios. Cierra al hacer click fuera. function MultiSelect({ label, icon, items, selected, onToggle, color = "#2563eb", bgOn = "#eff6ff", width = 240 }) { const [abierto, setAbierto] = useState(false); const ref = useRef(null); useEffect(() => { const fn = (e) => { if (ref.current && !ref.current.contains(e.target)) setAbierto(false); }; document.addEventListener("mousedown", fn); return () => document.removeEventListener("mousedown", fn); }, []); const n = selected.length; const resumen = n === 0 ? label : n === 1 ? (items.find((it) => it.v === selected[0])?.txt || `1 ${label}`) : `${n} seleccionados`; return (
{abierto && (
{items.length === 0 &&
Sin opciones.
} {items.map((it, i) => { const on = selected.includes(it.v); return ( ); })}
)}
); } // Lista desplegable de plantillas (selección única con casilla). Cierra al click fuera. function PlantillaSelect({ items, sel, onSelect, colorCat }) { const [abierto, setAbierto] = useState(false); const ref = useRef(null); useEffect(() => { const fn = (e) => { if (ref.current && !ref.current.contains(e.target)) setAbierto(false); }; document.addEventListener("mousedown", fn); return () => document.removeEventListener("mousedown", fn); }, []); return (
{abierto && (
{items.length === 0 &&
Sin plantillas.
} {items.map((p, i) => { const on = sel && sel.nombre === p.nombre; const c = colorCat(p.categoria); return ( ); })}
)}
); } export default function Plantillas({ ocultarTitulo = false }) { const [plantillas, setPlantillas] = useState([]); const [cargando, setCargando] = useState(true); const [error, setError] = useState(""); const [busca, setBusca] = useState(""); const [sel, setSel] = useState(null); // plantilla seleccionada // Envío const [telefonos, setTelefonos] = useState([]); // números leídos del Excel const [archivo, setArchivo] = useState(""); // nombre del archivo const [enviando, setEnviando] = useState(false); const [resultado, setResultado] = useState(null); // Campañas (leyendas) para armar base const [campanias, setCampanias] = useState([]); const [codsSel, setCodsSel] = useState([]); // códigos marcados const [buscandoBase, setBuscandoBase] = useState(false); const [baseMsg, setBaseMsg] = useState(""); // Asesores (filtro). "__VACIO__" = sin asesor. Por defecto: sin asesor marcado. const [asesores, setAsesores] = useState([]); const [asesSel, setAsesSel] = useState([]); // vacío = todos los asesores const [estados, setEstados] = useState([]); // estados/objeción disponibles const [estSel, setEstSel] = useState([]); // marcados (vacío = todos) const [orden, setOrden] = useState("desc"); // desc = más reciente→antiguo const [conteo, setConteo] = useState(null); // nº de teléfonos según filtros const [contando, setContando] = useState(false); const [limite, setLimite] = useState(""); // cuántos teléfonos exportar (vacío = todos) const [aviso, setAviso] = useState(""); // mini pop-up de advertencia const [enviandoBase, setEnviandoBase] = useState(false); const [confirmEnvio, setConfirmEnvio] = useState(null); // {n} para confirmar envío const [resEnvioBase, setResEnvioBase] = useState(null); // resultado del envío directo useEffect(() => { api.plantillas() .then((r) => { setPlantillas((r && r.plantillas) || []); setError(""); setCargando(false); }) .catch(() => { setError("No se pudieron cargar las plantillas."); setCargando(false); }); api.plantillasCampanias() .then((r) => setCampanias((r && r.campanias) || [])) .catch(() => {}); api.plantillasAsesores() .then((r) => setAsesores((r && r.asesores) || [])) .catch(() => {}); api.plantillasEstados() .then((r) => setEstados((r && r.estados) || [])) .catch(() => {}); }, []); // Cuenta los teléfonos apenas cambia un filtro (debounce para no saturar). useEffect(() => { if (codsSel.length === 0) { setConteo(null); setContando(false); return; } setContando(true); const t = setTimeout(() => { api.plantillasBase(codsSel, asesSel, estSel) .then((r) => setConteo(((r && r.filas) || []).length)) .catch(() => setConteo(null)) .finally(() => setContando(false)); }, 400); return () => clearTimeout(t); }, [codsSel, asesSel, estSel]); const toggleCod = (cod) => { setCodsSel((prev) => prev.includes(cod) ? prev.filter((c) => c !== cod) : [...prev, cod]); }; const toggleAsesor = (a) => { setAsesSel((prev) => prev.includes(a) ? prev.filter((x) => x !== a) : [...prev, a]); }; const toggleEstado = (e) => { setEstSel((prev) => prev.includes(e) ? prev.filter((x) => x !== e) : [...prev, e]); }; // Borra TODOS los filtros del paso 2 (campañas, asesor, estado, orden, cantidad). const limpiarFiltros = () => { setCodsSel([]); setAsesSel([]); setEstSel([]); setOrden("desc"); setLimite(""); setBaseMsg(""); setResEnvioBase(null); setConteo(null); }; // Validación en vivo del límite (solo cuando hay conteo y el campo tiene algo). const limiteError = (() => { if (conteo === null || limite === "") return ""; const n = parseInt(limite, 10); if (isNaN(n) || n < 1) return "El número debe ser 1 o mayor."; if (n > conteo) return `El número debe estar entre 1 y ${conteo}.`; return ""; })(); // Valida filtros/límite, trae las filas, las ordena por FECHA MENSAJE y corta a N. // Devuelve el array de filas listo, o null si hubo validación fallida (ya avisó). async function obtenerFilasFiltradas() { if (codsSel.length === 0) { setAviso("Debes marcar al menos una campaña."); return null; } const lim = parseInt(limite, 10); if (!limite || isNaN(lim)) { setAviso("Debes ingresar cuántos teléfonos."); return null; } if (lim < 1) { setAviso("El número debe ser 1 o mayor."); return null; } if (conteo !== null && lim > conteo) { setAviso(`El número debe estar entre 1 y ${conteo}.`); return null; } const r = await api.plantillasBase(codsSel, asesSel, estSel); let filas = (r && r.filas) || []; if (filas.length === 0) { setAviso("No se encontraron números para los filtros elegidos."); return null; } if (lim > filas.length) { setAviso(`El número debe estar entre 1 y ${filas.length}.`); return null; } filas.sort((a, b) => { const x = a.fecha_mensaje || "", y = b.fecha_mensaje || ""; if (!x && !y) return 0; if (!x) return 1; if (!y) return -1; return orden === "asc" ? (x < y ? -1 : x > y ? 1 : 0) : (x > y ? -1 : x < y ? 1 : 0); }); return filas.slice(0, lim); } // Exporta la base de los códigos marcados a Excel (teléfono + 2 fechas) async function exportarBase() { if (buscandoBase) return; setBuscandoBase(true); setBaseMsg(""); try { const filas = await obtenerFilasFiltradas(); if (!filas) { setBuscandoBase(false); return; } const XLSX = await import("xlsx"); // Serial de Excel a partir de YYYY-MM-DD (fecha REAL, ordenable/filtrable). const serialExcel = (iso) => { if (!iso) return ""; const [y, m, d] = iso.split("-").map(Number); if (!y || !m || !d) return iso; return Math.floor((Date.UTC(y, m - 1, d) - Date.UTC(1899, 11, 30)) / 86400000); }; // Construyo la hoja a mano para poder marcar las celdas como fecha (t:"d"/"n" con formato). const encabezados = ["TELEFONO", "FECHA MENSAJE", "ÚLTIMO MENSAJE", "ESTADO/OBJECIÓN", "ASESOR"]; const ws = {}; encabezados.forEach((h, c) => { ws[XLSX.utils.encode_cell({ r: 0, c })] = { v: h, t: "s" }; }); filas.forEach((f, ri) => { const r = ri + 1; ws[XLSX.utils.encode_cell({ r, c: 0 })] = { v: f.telefono, t: "s" }; const s1 = serialExcel(f.fecha_mensaje); const s2 = serialExcel(f.fecha_ult_mensaje); ws[XLSX.utils.encode_cell({ r, c: 1 })] = typeof s1 === "number" ? { v: s1, t: "n", z: "dd/mm/yyyy" } : { v: s1, t: "s" }; ws[XLSX.utils.encode_cell({ r, c: 2 })] = typeof s2 === "number" ? { v: s2, t: "n", z: "dd/mm/yyyy" } : { v: s2, t: "s" }; ws[XLSX.utils.encode_cell({ r, c: 3 })] = { v: f.estado || "", t: "s" }; ws[XLSX.utils.encode_cell({ r, c: 4 })] = { v: f.asesor || "", t: "s" }; }); ws["!ref"] = XLSX.utils.encode_range({ s: { r: 0, c: 0 }, e: { r: filas.length, c: 4 } }); ws["!cols"] = [{ wch: 16 }, { wch: 14 }, { wch: 16 }, { wch: 22 }, { wch: 20 }]; const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "Base"); XLSX.writeFile(wb, `base_${codsSel.join("-")}.xlsx`); setBaseMsg(`✅ ${filas.length} número(s) exportados.`); } catch (e) { setBaseMsg("Error: " + e.message); } setBuscandoBase(false); } // Paso 1: valida y pide confirmación antes de enviar a la base filtrada. async function pedirEnvioBase() { if (buscandoBase || enviandoBase) return; if (!sel) { setAviso("Elige una plantilla de la lista de arriba."); return; } setBuscandoBase(true); setBaseMsg(""); setResEnvioBase(null); try { const filas = await obtenerFilasFiltradas(); if (!filas) { setBuscandoBase(false); return; } setConfirmEnvio({ n: filas.length, filas }); } catch (e) { setBaseMsg("Error: " + e.message); } setBuscandoBase(false); } // Paso 2: envía la plantilla a los teléfonos ya filtrados (tras confirmar). async function enviarBaseConfirmado() { const filas = confirmEnvio && confirmEnvio.filas; setConfirmEnvio(null); if (!filas || !sel) return; setEnviandoBase(true); setResEnvioBase(null); setBaseMsg(""); try { const nums = filas.map((f) => f.telefono).filter(Boolean); const r = await api.plantillaEnviar({ nombre: sel.nombre, categoria: sel.categoria || "MARKETING", idioma: "es_PE", texto: sel.texto || "", telefonos: nums, }); setResEnvioBase(r); } catch (e) { setResEnvioBase({ error: "Error al enviar: " + e.message }); } setEnviandoBase(false); } const filtradas = useMemo(() => { const q = busca.trim().toLowerCase(); if (!q) return plantillas; return plantillas.filter((p) => (p.nombre || "").toLowerCase().includes(q) || (p.categoria || "").toLowerCase().includes(q)); }, [plantillas, busca]); const colorCat = (c) => { const u = String(c || "").toUpperCase(); if (u === "MARKETING") return { bg: "#eff6ff", fg: "#1e40af", bd: "#bfdbfe" }; if (u === "UTILITY") return { bg: "#ecfdf5", fg: "#047857", bd: "#a7f3d0" }; if (u === "AUTHENTICATION") return { bg: "#fffbeb", fg: "#b45309", bd: "#fde68a" }; return { bg: "#f1f5f9", fg: "#475569", bd: "#e2e8f0" }; }; // Leer el Excel y extraer la columna TELEFONO 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: "" }); // buscar la columna "telefono" (sin importar mayúsculas/espacios) 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); } async function enviar() { if (!sel || telefonos.length === 0 || enviando) return; setEnviando(true); setResultado(null); try { const r = await api.plantillaEnviar({ nombre: sel.nombre, categoria: sel.categoria || "MARKETING", idioma: "es_PE", texto: sel.texto || "", telefonos, }); setResultado(r); } catch (err) { setResultado({ error: "Error al enviar: " + err.message }); } setEnviando(false); } // Estilos reutilizables 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 0 36px" }; const fieldLabel = { fontSize: 12, fontWeight: 700, color: "#64748b", marginBottom: 7, textTransform: "uppercase", letterSpacing: ".02em" }; return (
{!ocultarTitulo &&

📄 Plantillas

} {/* PASO 1 — Elegir plantilla */}
1 Elige la plantilla {filtradas.length} disponibles
La plantilla que se enviará a los teléfonos.
setBusca(e.target.value)} placeholder="🔍 Buscar por nombre o categoría..." style={{ padding: "9px 14px", border: "1px solid #cbd5e1", borderRadius: 10, fontSize: 13, width: 280 }} />
{cargando ? (
Cargando plantillas...
) : error ? (
{error}
) : ( { setSel(p); setResultado(null); }} colorCat={colorCat} /> )}
{sel && (
✓ Plantilla: {sel.nombre}
)}
{/* PASO 2 — Armar base y enviar plantilla */}
2 Armar base y enviar plantilla
Marca campañas, aplica filtros y exporta o envía a esa base.
{/* Fila 1: campaña + asesor */}
🏷️ Campaña(s)
({ v: c.codigo, txt: c.codigo, title: (c.frases || []).join(" | ") }))} selected={codsSel} onToggle={toggleCod} color="#2563eb" bgOn="#eff6ff" width="100%" />
👤 Asesor
({ v: a, txt: a }))]} selected={asesSel} onToggle={toggleAsesor} color="#10b981" bgOn="#ecfdf5" width="100%" />
{/* Fila 2: estado + orden */}
🏷️ Estado / Objeción
({ v: e, txt: e }))} selected={estSel} onToggle={toggleEstado} color="#7c3aed" bgOn="#f5f3ff" width="100%" />
📅 Ordenar por fecha
{/* Fila 3: cantidad a enviar/exportar */}
🔢 Cantidad a enviar / exportar {conteo !== null && (1–{conteo})}
setLimite(e.target.value)} placeholder={conteo !== null ? `1 – ${conteo}` : "cantidad"} style={{ padding: "9px 14px", borderRadius: 10, fontSize: 13, fontWeight: 600, flex: 1, minWidth: 0, background: "#fff", color: "#0f172a", border: `1px solid ${limiteError ? "#ef4444" : "#cbd5e1"}` }} />
{limiteError && (
⚠️ {limiteError}
)}
{/* Barra de resumen + acciones */}
{codsSel.length === 0 ? "📱 marca una campaña" : contando ? "📱 contando…" : conteo === null ? "📱 —" : `📱 ${conteo} teléfono(s)`}
Se enviará: {sel ? {sel.nombre} : elige la plantilla en el paso 1 ↑}
{baseMsg && (
{baseMsg}
)} {resEnvioBase && (
{resEnvioBase.error ? resEnvioBase.error : ( <>✅ Enviados: {resEnvioBase.enviados} · ❌ Fallidos: {(resEnvioBase.fallidos || []).length} · ⚠️ Sin código 51: {(resEnvioBase.invalidos || []).length} )}
)}
{/* PASO 3 — Importar base y enviar plantilla */}
3 Importar base y enviar plantilla opcional
Si ya tienes una lista de teléfonos en Excel, impórtala y envía la plantilla.
{archivo && ( {archivo} · {telefonos.length} número(s) )}
{resultado && (
{resultado.error ? resultado.error : ( <> ✅ Enviados: {resultado.enviados} · ❌ Fallidos: {(resultado.fallidos || []).length} · ⚠️ Sin código 51 (no enviados): {(resultado.invalidos || []).length} {(resultado.invalidos || []).length > 0 && (
Rechazados por no tener código 51: {resultado.invalidos.join(", ")}
)} )}
)}
El Excel debe tener una columna TELEFONO. Solo se envía a números peruanos con código 51 (ej. 51966319975).
{/* Pop-up de confirmación de envío directo a la base */} {confirmEnvio && (
setConfirmEnvio(null)} style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,.35)", zIndex: 100, display: "flex", alignItems: "center", justifyContent: "center" }}>
e.stopPropagation()} style={{ background: "#fff", borderRadius: 14, padding: "24px 28px", maxWidth: 380, boxShadow: "0 20px 50px rgba(15,23,42,.25)", textAlign: "center" }}>
🚀
Enviar la plantilla {sel && sel.nombre} a
{confirmEnvio.n} teléfono(s)?
Esta acción no se puede deshacer.
)} {/* Mini pop-up de advertencia */} {aviso && (
setAviso("")} style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,.35)", zIndex: 100, display: "flex", alignItems: "center", justifyContent: "center" }}>
e.stopPropagation()} style={{ background: "#fff", borderRadius: 14, padding: "22px 26px", maxWidth: 340, boxShadow: "0 20px 50px rgba(15,23,42,.25)", textAlign: "center" }}>
⚠️
{aviso}
)}
); }