634 lines
32 KiB
JavaScript
634 lines
32 KiB
JavaScript
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>
|
||
);
|
||
}
|