/* global React, IconChart, IconSearch, IconCalendar, IconInbox, IconUsers, IconLogout, IconSidebar, IconPlug, PipelineView, AnalyticsView, CallsView, IntegracionesView, Drawer, LoginScreen, UsersView, Avatar */ // REVOLVR CRM — shell. Auth + roles + switch de pipeline por closer + vistas + drawer. const { useState, useEffect, useRef } = React; // Supabase usa snake_case; el CRM usa camelCase. Convertidor de filas. function dbLeadToCRM(row) { return { id: row.id, name: row.name, email: row.email, phone: row.phone || "", location: row.location || "", timezone: row.timezone || "", eventType: row.event_type || "", duration: row.duration || 30, scheduledAt: row.scheduled_at ? new Date(row.scheduled_at) : new Date(), closerId: row.closer_id || "", stage: row.stage || "agendada", status: row.status || "agendada", meetingLink: row.meeting_link || "", form: row.form_data || {}, utm: row.utm || { source: "", medium: "", campaign: "" }, value: Number(row.value) || 0, notes: row.notes || [], history: row.history || [], source: row.source || "manual", createdAt: row.created_at ? new Date(row.created_at) : new Date(), }; } function CRMApp({ theme = "atrio" }) { const D = window.REVOLVR_DATA; const [currentUser, setCurrentUser] = useState(() => { try { const saved = localStorage.getItem("crm_session"); if (!saved) return null; const u = JSON.parse(saved); // Verificar que el usuario sigue existiendo en REVOLVR_DATA const valid = window.REVOLVR_DATA.USERS.find((x) => x.id === u.id && x.active); return valid ? u : null; } catch { return null; } }); const handleLogin = (u) => { localStorage.setItem("crm_session", JSON.stringify(u)); setCurrentUser(u); }; const handleLogout = () => { localStorage.removeItem("crm_session"); setCurrentUser(null); }; if (!currentUser) { return (
); } return ; } function CRMShell({ theme, currentUser, onLogout }) { const D = window.REVOLVR_DATA; const isAdmin = currentUser.role === "admin"; const [tab, setTab] = useState("pipeline"); const [stages, setStages] = useState(() => D.STAGES.map((s) => ({ ...s }))); const [calls, setCalls] = useState([]); // Supabase carga los leads — data.js solo es fallback const [loadingLeads, setLoadingLeads] = useState(false); const [users, setUsers] = useState([]); const [closers, setClosers] = useState([]); const [openId, setOpenId] = useState(null); const [filters, setFilters] = useState({ source: null, q: "", dateRange: null }); const [activeCloser, setActiveCloser] = useState(isAdmin ? "all" : currentUser.closerId); const [adSpend, setAdSpend] = useState(() => ({ ...D.AD_SPEND })); const [justMovedId, setJustMovedId] = useState(null); const justMovedTimer = useRef(null); const [toasts, setToasts] = useState([]); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [userMenu, setUserMenu] = useState(false); const userMenuRef = useRef(null); useEffect(() => { if (!userMenu) return; const onDown = (e) => { if (userMenuRef.current && !userMenuRef.current.contains(e.target)) setUserMenu(false); }; document.addEventListener("mousedown", onDown); return () => document.removeEventListener("mousedown", onDown); }, [userMenu]); // Carga leads y usuarios reales desde Supabase al montar useEffect(() => { if (!window.SUPABASE) return; setLoadingLeads(true); // Cargar leads window.SUPABASE.from("leads").select("*").order("scheduled_at", { ascending: false }) .then(({ data, error }) => { if (!error && data && data.length > 0) setCalls(data.map(dbLeadToCRM)); setLoadingLeads(false); }); // Cargar usuarios window.SUPABASE.from("crm_users").select("*").order("created_at") .then(({ data, error }) => { if (!error && data && data.length > 0) { setUsers(data.map((u) => ({ id: u.id, name: u.name, email: u.email, password: u.password, role: u.role, closerId: u.closer_id, active: u.active, lastLogin: u.last_login || "" }))); } }); // Cargar closers window.SUPABASE.from("closers").select("*").order("order_index") .then(({ data, error }) => { if (!error && data) { setClosers(data.map((c) => ({ id: c.id, name: c.name, initials: c.initials, color: c.color }))); } }); }, []); const viewCloser = isAdmin ? activeCloser : currentUser.closerId; const globalScope = isAdmin && viewCloser === "all"; const visibleCalls = calls.filter((c) => viewCloser === "all" ? true : c.closerId === viewCloser); const pushToast = (text, kind = "ok") => { const id = Math.random().toString(36).slice(2); setToasts((t) => [...t, { id, text, kind }]); setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 4000); }; const handleOpen = (id) => setOpenId(id); const handleMove = (id, newStage, beforeId) => { setCalls((cs) => { const moving = cs.find((c) => c.id === id); if (!moving) return cs; const sameStage = moving.stage === newStage; const fromLabel = stages.find((s) => s.id === moving.stage)?.label; const toLabel = stages.find((s) => s.id === newStage)?.label; const statusMap = { asistio: "asistio", noshow: "noshow", confirmada: "confirmada", agendada: "agendada" }; const status = sameStage ? moving.status : (statusMap[newStage] || moving.status); const updated = sameStage ? { ...moving } : { ...moving, stage: newStage, status, history: [{ ts: "ahora", kind: "stage", e: "Etapa cambiada", det: `${fromLabel} → ${toLabel}` }, ...moving.history], }; const rest = cs.filter((c) => c.id !== id); let idx; if (beforeId) { idx = rest.findIndex((c) => c.id === beforeId); if (idx < 0) idx = rest.length; } else { // sin destino explícito: colócalo después de la última tarjeta de esa etapa let last = -1; rest.forEach((c, i) => { if (c.stage === newStage) last = i; }); idx = last + 1; } rest.splice(idx, 0, updated); return rest; }); setJustMovedId(id); if (justMovedTimer.current) clearTimeout(justMovedTimer.current); justMovedTimer.current = setTimeout(() => setJustMovedId(null), 600); if (window.SUPABASE) { const statusMap = { asistio: "asistio", noshow: "noshow", confirmada: "confirmada", agendada: "agendada", propuesta: "asistio", ganada: "asistio", perdida: "asistio" }; window.SUPABASE.from("leads") .update({ stage: newStage, status: statusMap[newStage] || newStage }) .eq("id", id) .then(({ error }) => { if (error) console.error("Error guardando etapa:", error); }); } }; const handleChangeStage = handleMove; const handleChangeCloser = (id, closerId) => { setCalls((cs) => cs.map((c) => { if (c.id !== id) return c; const to = closers.find((x) => x.id === closerId)?.name; return { ...c, closerId, history: [{ ts: "ahora", kind: "note", e: "Llamada reasignada", det: `a ${to}` }, ...c.history] }; })); pushToast("Llamada reasignada"); }; const handleAddNote = (id, body) => { const newNote = { ts: "ahora", who: currentUser.name, body }; const newHistEntry = { ts: "ahora", kind: "note", e: "Nota agregada", det: `por ${currentUser.name}` }; setCalls((cs) => cs.map((c) => c.id !== id ? c : { ...c, notes: [newNote, ...c.notes], history: [newHistEntry, ...c.history], })); if (window.SUPABASE) { const lead = calls.find((c) => c.id === id); const updatedNotes = lead ? [newNote, ...lead.notes] : [newNote]; const updatedHistory = lead ? [newHistEntry, ...lead.history] : [newHistEntry]; window.SUPABASE.from("leads") .update({ notes: updatedNotes, history: updatedHistory }) .eq("id", id) .then(({ error }) => { if (error) console.error("Error guardando nota:", error); }); } }; const handleEditNote = (id, noteIndex, newBody) => { setCalls((cs) => cs.map((c) => { if (c.id !== id) return c; const updatedNotes = c.notes.map((n, i) => i === noteIndex ? { ...n, body: newBody, ts: "editada" } : n); if (window.SUPABASE) { window.SUPABASE.from("leads").update({ notes: updatedNotes }).eq("id", id) .then(({ error }) => { if (error) console.error("Error editando nota:", error); }); } return { ...c, notes: updatedNotes }; })); }; const handleDeleteNote = (id, noteIndex) => { setCalls((cs) => cs.map((c) => { if (c.id !== id) return c; const updatedNotes = c.notes.filter((_, i) => i !== noteIndex); if (window.SUPABASE) { window.SUPABASE.from("leads").update({ notes: updatedNotes }).eq("id", id) .then(({ error }) => { if (error) console.error("Error borrando nota:", error); }); } return { ...c, notes: updatedNotes }; })); pushToast("Nota eliminada"); }; const handleUpdateSale = (id, sale) => { setCalls((cs) => cs.map((c) => c.id !== id ? c : { ...c, sale, history: [{ ts: "ahora", kind: "stage", e: "Venta registrada", det: `${sale.programa} · $${(sale.total || 0).toLocaleString("en-US")} · ${sale.tipo === "total" ? "pago total" : "abono"}` }, ...c.history], })); pushToast("Venta actualizada"); }; // Stage CRUD const handleAddStage = (label, color) => setStages((sx) => [...sx, { id: "s_" + Date.now(), label, color }]); const handleRenameStage = (id, label) => setStages((sx) => sx.map((s) => s.id === id ? { ...s, label } : s)); const handleRecolorStage = (id, color) => setStages((sx) => sx.map((s) => s.id === id ? { ...s, color } : s)); const handleMoveStage = (id, dir) => setStages((sx) => { const i = sx.findIndex((s) => s.id === id); const n = i + dir; if (i < 0 || n < 0 || n >= sx.length) return sx; const out = sx.slice(); [out[i], out[n]] = [out[n], out[i]]; return out; }); const handleReorderStages = (fromId, toId) => setStages((sx) => { const f = sx.findIndex((s) => s.id === fromId), t = sx.findIndex((s) => s.id === toId); if (f < 0 || t < 0 || f === t) return sx; const out = sx.slice(); const [m] = out.splice(f, 1); out.splice(t, 0, m); return out; }); const handleDeleteStage = (id, moveTo) => { if (moveTo) setCalls((cs) => cs.map((c) => c.stage === id ? { ...c, stage: moveTo } : c)); setStages((sx) => sx.filter((s) => s.id !== id)); }; const handleExport = (rows) => { const headers = ["nombre", "email", "telefono", "ubicacion", "evento", "fecha", "closer", "etapa", "estado", "fuente", "campaña", "facturacion"]; const data = rows.map((c) => [ c.name, c.email, c.phone, c.location, c.eventType, c.scheduledAt instanceof Date ? c.scheduledAt.toLocaleString("es-ES") : "", closers.find((x) => x.id === c.closerId)?.name || "", c.stage, c.status, c.utm.source, c.utm.campaign, c.form.facturacion, ]); const esc = (v) => `"${String(v == null ? "" : v).replace(/"/g, '""')}"`; const csv = [headers, ...data].map((r) => r.map(esc).join(",")).join("\n"); const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `llamadas_${new Date().toISOString().slice(0, 10)}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); pushToast(`${rows.length} llamadas exportadas`); }; const handleAddUser = (u) => { const newId = "u_" + Date.now(); let closerId = u.closerId; if (u.role === "closer" && u.closerId === "new") { closerId = "c_" + Date.now(); const newCloser = { id: closerId, name: u.name, initials: u.name.split(" ").map((s) => s[0]).slice(0, 2).join("").toUpperCase(), color: "#0e7a9a" }; setClosers((cs) => [...cs, newCloser]); if (window.SUPABASE) { window.SUPABASE.from("closers").insert({ id: closerId, name: u.name, initials: newCloser.initials, color: "#0e7a9a", active: true, order_index: D.CLOSERS.length }) .then(({ error }) => { if (error) console.error("Error guardando closer:", error); }); } } const newUser = { id: newId, name: u.name, email: u.email, password: u.password, role: u.role, closerId: u.role === "admin" ? null : closerId, active: true, lastLogin: "nunca" }; setUsers((us) => [...us, newUser]); if (window.SUPABASE) { window.SUPABASE.from("crm_users").insert({ id: newId, name: u.name, email: u.email, password: u.password, role: u.role, closer_id: u.role === "admin" ? null : closerId, active: true }) .then(({ error }) => { if (error) console.error("Error guardando usuario:", error); }); } pushToast("Usuario creado"); }; const handleToggleActive = (id) => { setUsers((us) => us.map((u) => u.id === id ? { ...u, active: !u.active } : u)); const u = users.find((x) => x.id === id); if (window.SUPABASE && u) { window.SUPABASE.from("crm_users").update({ active: !u.active }).eq("id", id) .then(({ error }) => { if (error) console.error("Error actualizando usuario:", error); }); } }; const handleResetPassword = (id, newPassword) => { setUsers((us) => us.map((u) => u.id === id ? { ...u, password: newPassword } : u)); if (window.SUPABASE) { window.SUPABASE.from("crm_users").update({ password: newPassword }).eq("id", id) .then(({ error }) => { if (error) console.error("Error reseteando contraseña:", error); }); } pushToast("Contraseña actualizada"); }; const handleDeleteUser = (id) => { setUsers((us) => us.filter((u) => u.id !== id)); if (window.SUPABASE) { window.SUPABASE.from("crm_users").delete().eq("id", id) .then(({ error }) => { if (error) console.error("Error eliminando usuario:", error); }); } pushToast("Usuario eliminado"); }; const open = calls.find((c) => c.id === openId); const counts = D.countByStage(visibleCalls); const closer = closers.find((x) => x.id === currentUser.closerId); const pageTitle = tab === "pipeline" ? "Pipeline" : tab === "llamadas" ? "Próximas llamadas" : tab === "analitica" ? "Analítica" : tab === "integraciones" ? "Integraciones" : "Usuarios"; return (
{/* Sidebar */} {/* Main */}
REVOLVR/{pageTitle} {tab !== "usuarios" && tab !== "integraciones" && isAdmin && {viewCloser === "all" ? "Todos" : closers.find((c) => c.id === viewCloser)?.name}}
{tab !== "usuarios" && tab !== "integraciones" &&
setFilters({ ...filters, q: e.target.value })} placeholder="Buscar nombre, email, industria…"/>
} {/* User menu */}
{userMenu &&
{currentUser.name}
{currentUser.email}
{isAdmin ? "Administrador" : "Closer"}
}
{loadingLeads && (
)} {tab === "pipeline" && } {tab === "llamadas" && } {tab === "analitica" && } {tab === "usuarios" && isAdmin && } {tab === "integraciones" && isAdmin && }
{open && setOpenId(null)} onChangeStage={handleChangeStage} onChangeCloser={handleChangeCloser} onAddNote={handleAddNote} onEditNote={handleEditNote} onDeleteNote={handleDeleteNote} onUpdateSale={handleUpdateSale}/>}
{toasts.map((t) => (
{t.text}
))}
); } function NavItem({ icon, children, active, onClick, badge }) { return ( ); } window.CRMApp = CRMApp;