Añadir frontend/src/pages/Plantillas.jsx

This commit is contained in:
2026-09-19 11:25:26 -05:00
parent e3c8862aa0
commit fd0d9549cf

View File

@@ -0,0 +1,633 @@
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 (
<div ref={ref} style={{ position: "relative", width }}>
<button type="button" onClick={() => setAbierto((a) => !a)}
style={{ width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between",
gap: 8, padding: "9px 14px", borderRadius: 10, border: "1px solid #cbd5e1",
background: "#fff", color: n ? "#0f172a" : "#94a3b8", fontSize: 13, fontWeight: 600,
cursor: "pointer" }}>
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{icon} {resumen}
</span>
<span style={{ color: "#94a3b8", fontSize: 11 }}>▼</span>
</button>
{abierto && (
<div style={{ position: "absolute", zIndex: 30, top: "calc(100% + 4px)", left: 0, right: 0,
background: "#fff", border: "1px solid #e2e8f0", borderRadius: 10,
boxShadow: "0 8px 24px rgba(15,23,42,.14)", maxHeight: 280, overflowY: "auto", padding: 6 }}>
{items.length === 0 && <div style={{ padding: 10, color: "#94a3b8", fontSize: 13 }}>Sin opciones.</div>}
{items.map((it, i) => {
const on = selected.includes(it.v);
return (
<label key={i} onClick={() => onToggle(it.v)} title={it.title || ""}
style={{ display: "flex", alignItems: "center", gap: 9, padding: "7px 10px", borderRadius: 8,
cursor: "pointer", fontSize: 13, fontWeight: 600,
background: on ? bgOn : "transparent", color: on ? color : "#475569" }}>
<span style={{ width: 16, height: 16, borderRadius: 5, flexShrink: 0, display: "flex",
alignItems: "center", justifyContent: "center",
border: on ? "none" : "1.5px solid #cbd5e1", background: on ? color : "#fff" }}>
{on && <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>}
</span>
{it.txt}
</label>
);
})}
</div>
)}
</div>
);
}
// 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 (
<div ref={ref} style={{ position: "relative", maxWidth: 480 }}>
<button type="button" onClick={() => setAbierto((a) => !a)}
style={{ width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between",
gap: 8, padding: "11px 16px", borderRadius: 10, border: "1px solid #cbd5e1",
background: "#fff", color: sel ? "#0f172a" : "#94a3b8", fontSize: 13.5, fontWeight: 600,
cursor: "pointer" }}>
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
📄 {sel ? sel.nombre : "Elige una plantilla..."}
</span>
<span style={{ color: "#94a3b8", fontSize: 11 }}>▼</span>
</button>
{abierto && (
<div style={{ position: "absolute", zIndex: 30, top: "calc(100% + 4px)", left: 0, right: 0,
background: "#fff", border: "1px solid #e2e8f0", borderRadius: 10,
boxShadow: "0 8px 24px rgba(15,23,42,.14)", maxHeight: 340, overflowY: "auto", padding: 6 }}>
{items.length === 0 && <div style={{ padding: 10, color: "#94a3b8", fontSize: 13 }}>Sin plantillas.</div>}
{items.map((p, i) => {
const on = sel && sel.nombre === p.nombre;
const c = colorCat(p.categoria);
return (
<label key={i} onClick={() => { onSelect(p); setAbierto(false); }}
style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 10px", borderRadius: 8,
cursor: "pointer", fontSize: 13, background: on ? "#eff6ff" : "transparent" }}>
<span style={{ width: 16, height: 16, borderRadius: "50%", flexShrink: 0, display: "flex",
alignItems: "center", justifyContent: "center",
border: on ? "5px solid #2563eb" : "1.5px solid #cbd5e1", background: "#fff" }} />
<span style={{ fontWeight: 600, color: "#0f172a", flex: 1, overflow: "hidden",
textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.nombre || "—"}</span>
<span style={{ display: "inline-block", padding: "2px 10px", borderRadius: 999, fontSize: 11,
fontWeight: 700, background: c.bg, color: c.fg, border: `1px solid ${c.bd}` }}>
{p.categoria || "—"}
</span>
</label>
);
})}
</div>
)}
</div>
);
}
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 (
<div style={{ display: "flex", flexDirection: "column", gap: 16, maxWidth: 1080 }}>
{!ocultarTitulo && <h1 className="page-title">📄 Plantillas</h1>}
{/* PASO 1 — Elegir plantilla */}
<div style={card}>
<div style={cardHead}>
<span style={step}>1</span>
<span style={cardTitle}>Elige la plantilla</span>
<span style={{ marginLeft: "auto", fontSize: 12, color: "#94a3b8", fontWeight: 600 }}>
{filtradas.length} disponibles
</span>
</div>
<div style={cardSub}>La plantilla que se enviará a los teléfonos.</div>
<div style={{ display: "flex", gap: 10, marginTop: 14, flexWrap: "wrap", alignItems: "center" }}>
<input value={busca} onChange={(e) => 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 }} />
<div style={{ flex: 1, minWidth: 260 }}>
{cargando ? (
<div style={{ padding: "11px 14px", color: "#94a3b8", fontSize: 13 }}>Cargando plantillas...</div>
) : error ? (
<div style={{ padding: "11px 14px", color: "#b91c1c", background: "#fef2f2",
border: "1px solid #fecaca", borderRadius: 10, fontSize: 13 }}>{error}</div>
) : (
<PlantillaSelect items={filtradas} sel={sel}
onSelect={(p) => { setSel(p); setResultado(null); }} colorCat={colorCat} />
)}
</div>
</div>
{sel && (
<div style={{ marginTop: 12, display: "inline-flex", alignItems: "center", gap: 8,
padding: "7px 14px", borderRadius: 999, background: "#eff6ff",
border: "1px solid #bfdbfe", fontSize: 13 }}>
<span style={{ color: "#1e40af", fontWeight: 700 }}>✓ Plantilla:</span>
<b style={{ color: "#1e3a5f" }}>{sel.nombre}</b>
</div>
)}
</div>
{/* PASO 2 — Armar base y enviar plantilla */}
<div style={card}>
<div style={cardHead}>
<span style={step}>2</span>
<span style={cardTitle}>Armar base y enviar plantilla</span>
</div>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, flexWrap: "wrap" }}>
<div style={cardSub}>Marca campañas, aplica filtros y exporta o envía a esa base.</div>
<button type="button" onClick={limpiarFiltros}
style={{ padding: "6px 14px", borderRadius: 999, border: "1px solid #e2e8f0",
background: "#f8fafc", color: "#64748b", fontSize: 12, fontWeight: 700,
cursor: "pointer", whiteSpace: "nowrap" }}>
↺ Limpiar filtros
</button>
</div>
{/* Fila 1: campaña + asesor */}
<div style={{ marginTop: 16, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
<div>
<div style={fieldLabel}>🏷️ Campaña(s)</div>
<MultiSelect label="campaña" icon="🏷️"
items={campanias.map((c) => ({ v: c.codigo, txt: c.codigo, title: (c.frases || []).join(" | ") }))}
selected={codsSel} onToggle={toggleCod}
color="#2563eb" bgOn="#eff6ff" width="100%" />
</div>
<div>
<div style={fieldLabel}>👤 Asesor</div>
<MultiSelect label="todos" icon="👤"
items={[{ v: "__VACIO__", txt: "Sin asesor" }, ...asesores.map((a) => ({ v: a, txt: a }))]}
selected={asesSel} onToggle={toggleAsesor}
color="#10b981" bgOn="#ecfdf5" width="100%" />
</div>
</div>
{/* Fila 2: estado + orden */}
<div style={{ marginTop: 14, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
<div>
<div style={fieldLabel}>🏷️ Estado / Objeción</div>
<MultiSelect label="todos" icon="🏷️"
items={estados.map((e) => ({ v: e, txt: e }))}
selected={estSel} onToggle={toggleEstado}
color="#7c3aed" bgOn="#f5f3ff" width="100%" />
</div>
<div>
<div style={fieldLabel}>📅 Ordenar por fecha</div>
<select value={orden} onChange={(e) => setOrden(e.target.value)}
style={{ padding: "9px 14px", borderRadius: 10, border: "1px solid #cbd5e1",
background: "#fff", color: "#0f172a", fontSize: 13, fontWeight: 600,
cursor: "pointer", width: "100%" }}>
<option value="desc">Más reciente → antiguo</option>
<option value="asc">Más antiguo → reciente</option>
</select>
</div>
</div>
{/* Fila 3: cantidad a enviar/exportar */}
<div style={{ marginTop: 14 }}>
<div style={fieldLabel}>
🔢 Cantidad a enviar / exportar {conteo !== null && <span style={{ color: "#94a3b8" }}>(1–{conteo})</span>}
</div>
<div style={{ display: "flex", gap: 8, maxWidth: 320 }}>
<input type="number" min={1} max={conteo || undefined} value={limite}
onChange={(e) => 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"}` }} />
<button type="button" onClick={() => conteo !== null && setLimite(String(conteo))}
disabled={conteo === null}
style={{ padding: "9px 16px", borderRadius: 10, border: "1px solid #cbd5e1",
background: conteo === null ? "#f1f5f9" : "#fff", color: "#334155",
fontWeight: 700, fontSize: 12.5, cursor: conteo === null ? "default" : "pointer" }}>
Todos
</button>
</div>
{limiteError && (
<div style={{ marginTop: 6, fontSize: 12, fontWeight: 600, color: "#dc2626" }}>⚠️ {limiteError}</div>
)}
</div>
{/* Barra de resumen + acciones */}
<div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 18, paddingTop: 16,
borderTop: "1px solid #eef2f7", flexWrap: "wrap" }}>
<span style={{ fontSize: 13, fontWeight: 700, padding: "7px 14px", borderRadius: 999,
background: "#eff6ff", color: "#1e40af", border: "1px solid #bfdbfe" }}>
{codsSel.length === 0 ? "📱 marca una campaña"
: contando ? "📱 contando…"
: conteo === null ? "📱 —"
: `📱 ${conteo} teléfono(s)`}
</span>
<div style={{ flex: 1 }} />
<button onClick={exportarBase} disabled={buscandoBase || enviandoBase || !!limiteError}
style={{ padding: "10px 18px", borderRadius: 10, border: "1px solid #cbd5e1",
background: (buscandoBase || enviandoBase || limiteError) ? "#f1f5f9" : "#fff",
color: "#334155", fontWeight: 700, fontSize: 13,
cursor: (buscandoBase || enviandoBase || limiteError) ? "default" : "pointer" }}>
{buscandoBase ? "Buscando..." : "⬇️ Exportar Excel"}
</button>
<button onClick={pedirEnvioBase} disabled={buscandoBase || enviandoBase || !!limiteError}
style={{ padding: "10px 20px", borderRadius: 10, border: "none",
background: (buscandoBase || enviandoBase || limiteError)
? "#cbd5e1" : "linear-gradient(135deg,#1e3a5f,#2d5580)",
color: "#fff", fontWeight: 700, fontSize: 13,
cursor: (buscandoBase || enviandoBase || limiteError) ? "default" : "pointer" }}>
{enviandoBase ? "Enviando..." : "🚀 Enviar a esta base"}
</button>
</div>
<div style={{ marginTop: 10, fontSize: 12.5 }}>
<span style={{ color: "#94a3b8" }}>Se enviará: </span>
{sel ? <b style={{ color: "#1e3a5f" }}>{sel.nombre}</b>
: <span style={{ color: "#94a3b8" }}>elige la plantilla en el paso 1 ↑</span>}
</div>
{baseMsg && (
<div style={{ marginTop: 8, fontSize: 12.5, fontWeight: 600,
color: baseMsg.startsWith("✅") ? "#047857" : "#b45309" }}>{baseMsg}</div>
)}
{resEnvioBase && (
<div style={{ marginTop: 12, padding: "12px 14px", borderRadius: 10, fontSize: 13,
background: resEnvioBase.error ? "#fef2f2" : "#ecfdf5",
color: resEnvioBase.error ? "#991b1b" : "#065f46",
border: `1px solid ${resEnvioBase.error ? "#fecaca" : "#a7f3d0"}` }}>
{resEnvioBase.error ? resEnvioBase.error : (
<>✅ Enviados: <b>{resEnvioBase.enviados}</b> ·
❌ Fallidos: <b>{(resEnvioBase.fallidos || []).length}</b> ·
⚠️ Sin código 51: <b>{(resEnvioBase.invalidos || []).length}</b></>
)}
</div>
)}
</div>
{/* PASO 3 — Importar base y enviar plantilla */}
<div style={card}>
<div style={cardHead}>
<span style={{ ...step, background: "#64748b" }}>3</span>
<span style={cardTitle}>Importar base y enviar plantilla</span>
<span style={{ marginLeft: "auto", fontSize: 11.5, color: "#94a3b8", fontWeight: 600 }}>opcional</span>
</div>
<div style={cardSub}>Si ya tienes una lista de teléfonos en Excel, impórtala y envía la plantilla.</div>
<div style={{ display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap", marginTop: 16 }}>
<label style={{ display: "inline-flex", alignItems: "center", gap: 8, padding: "10px 16px",
borderRadius: 10, border: "1px solid #cbd5e1", background: "#f8fafc",
cursor: "pointer", fontSize: 13, fontWeight: 600, color: "#334155" }}>
📁 Importar Excel
<input type="file" accept=".xlsx,.xls" onChange={importarExcel} style={{ display: "none" }} />
</label>
{archivo && (
<span style={{ fontSize: 12.5, color: "#475569" }}>
{archivo} · <b>{telefonos.length}</b> número(s)
</span>
)}
<button onClick={enviar} disabled={!sel || telefonos.length === 0 || enviando}
style={{ marginLeft: "auto", padding: "10px 22px", borderRadius: 10, border: "none",
background: (!sel || telefonos.length === 0 || enviando)
? "#cbd5e1" : "linear-gradient(135deg,#1e3a5f,#2d5580)",
color: "#fff", fontWeight: 700, fontSize: 13.5,
cursor: (!sel || telefonos.length === 0 || enviando) ? "default" : "pointer" }}>
{enviando ? "Enviando..." : "🚀 Enviar plantilla"}
</button>
</div>
{resultado && (
<div style={{ marginTop: 14, padding: "12px 14px", borderRadius: 10, fontSize: 13,
background: resultado.error ? "#fef2f2" : "#ecfdf5",
color: resultado.error ? "#991b1b" : "#065f46",
border: `1px solid ${resultado.error ? "#fecaca" : "#a7f3d0"}` }}>
{resultado.error ? resultado.error : (
<>
✅ Enviados: <b>{resultado.enviados}</b> ·
❌ Fallidos: <b>{(resultado.fallidos || []).length}</b> ·
⚠️ Sin código 51 (no enviados): <b>{(resultado.invalidos || []).length}</b>
{(resultado.invalidos || []).length > 0 && (
<div style={{ marginTop: 6, fontSize: 12, color: "#b45309" }}>
Rechazados por no tener código 51: {resultado.invalidos.join(", ")}
</div>
)}
</>
)}
</div>
)}
<div style={{ marginTop: 10, fontSize: 11.5, color: "#94a3b8" }}>
El Excel debe tener una columna <b>TELEFONO</b>. Solo se envía a números peruanos con código <b>51</b> (ej. 51966319975).
</div>
</div>
{/* Pop-up de confirmación de envío directo a la base */}
{confirmEnvio && (
<div onClick={() => setConfirmEnvio(null)}
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,.35)", zIndex: 100,
display: "flex", alignItems: "center", justifyContent: "center" }}>
<div onClick={(e) => e.stopPropagation()}
style={{ background: "#fff", borderRadius: 14, padding: "24px 28px", maxWidth: 380,
boxShadow: "0 20px 50px rgba(15,23,42,.25)", textAlign: "center" }}>
<div style={{ fontSize: 34, marginBottom: 8 }}>🚀</div>
<div style={{ fontSize: 14.5, fontWeight: 600, color: "#0f172a", marginBottom: 6 }}>
Enviar la plantilla <b>{sel && sel.nombre}</b> a<br /><b>{confirmEnvio.n}</b> teléfono(s)?
</div>
<div style={{ fontSize: 12, color: "#94a3b8", marginBottom: 18 }}>Esta acción no se puede deshacer.</div>
<div style={{ display: "flex", gap: 10, justifyContent: "center" }}>
<button onClick={() => setConfirmEnvio(null)}
style={{ padding: "9px 22px", borderRadius: 10, border: "1px solid #cbd5e1",
background: "#fff", color: "#334155", fontWeight: 700, fontSize: 13.5, cursor: "pointer" }}>
Cancelar
</button>
<button onClick={enviarBaseConfirmado}
style={{ padding: "9px 22px", borderRadius: 10, border: "none",
background: "linear-gradient(135deg,#1e3a5f,#2d5580)", color: "#fff",
fontWeight: 700, fontSize: 13.5, cursor: "pointer" }}>
Sí, enviar
</button>
</div>
</div>
</div>
)}
{/* Mini pop-up de advertencia */}
{aviso && (
<div onClick={() => setAviso("")}
style={{ position: "fixed", inset: 0, background: "rgba(15,23,42,.35)", zIndex: 100,
display: "flex", alignItems: "center", justifyContent: "center" }}>
<div onClick={(e) => e.stopPropagation()}
style={{ background: "#fff", borderRadius: 14, padding: "22px 26px", maxWidth: 340,
boxShadow: "0 20px 50px rgba(15,23,42,.25)", textAlign: "center" }}>
<div style={{ fontSize: 34, marginBottom: 8 }}>⚠️</div>
<div style={{ fontSize: 14.5, fontWeight: 600, color: "#0f172a", marginBottom: 18 }}>{aviso}</div>
<button onClick={() => setAviso("")}
style={{ padding: "9px 26px", borderRadius: 10, border: "none",
background: "linear-gradient(135deg,#1e3a5f,#2d5580)", color: "#fff",
fontWeight: 700, fontSize: 13.5, cursor: "pointer" }}>
Entendido
</button>
</div>
</div>
)}
</div>
);
}