Files
dashboard-leads/frontend/src/pages/Plantillas.jsx

634 lines
32 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}