/* ============================================================ admin.jsx — shell (sidebar+topbar) + Dashboard + Sales + Customers + Settings ============================================================ */ const NAV = [ { key: "dashboard", label: "Dashboard", icon: "dash" }, { key: "checkouts", label: "Criar checkout", icon: "plus", primary: true }, { key: "products", label: "Produtos", icon: "tag" }, { key: "bumps", label: "Order bumps", icon: "bolt" }, { key: "sales", label: "Vendas", icon: "cart" }, { key: "customers", label: "Clientes", icon: "users" }, { key: "settings", label: "Configurações",icon: "set" }, ]; function Sidebar({ active, onNav }){ const { setUser, setRoute, sales } = useApp(); const pendings = sales.filter(s => s.status === "pendente").length; return ( ); } function Topbar({ title, eyebrow, actions }){ return (
{eyebrow && {eyebrow}}

{title}

{actions}
); } function Shell({ title, eyebrow, page, actions, children }){ const { setRoute } = useApp(); return (
setRoute("/" + k)} />
{children}
); } // ============================================================ // Dashboard // ============================================================ function Dashboard(){ const { sales, series, setRoute } = useApp(); const paid = sales.filter(s => s.status === "pago"); const pending = sales.filter(s => s.status === "pendente"); const totalSales = paid.reduce((a, s) => a + s.amount, 0); const pendingAmount = pending.reduce((a, s) => a + s.amount, 0); const newCustomers = 6; const conversionRate = 38.4; return ( setRoute("/checkouts")}> Novo checkout}>
Resumo

Cobrar com presença, medir com clareza.

Acompanhamento em tempo real das suas vendas, links gerados e cobranças pendentes integradas ao Mercado Pago.

Receita aprovada
{fmtBRL(totalSales)}
+18,2% vs. semana anterior
Cobranças pendentes
{fmtBRL(pendingAmount)}
{pending.length} aguardando confirmação
v * 0.4)} />
Conversão de checkout
{conversionRate}%
+2,1 p.p.
40 + Math.sin(i / 2) * 8 + (i / 8))} />
Novos clientes
{newCustomers}
últimas 24h

Receita por dia

Últimos 29 dias · timezone Brasília
s.method === "pix").length / paid.length * 100)}%`} /> s.method === "card").length / paid.length * 100)}%`} />

Atividade recente

{ e.preventDefault(); setRoute("/sales"); }} aria-label="Ver todas">
{sales.slice(0, 6).map((s) => (
{initials(s.customer)}
{s.customer} {s.product} · {ago(s.date)}
{fmtBRL(s.amount)}
))}
setRoute("/checkouts")} /> setRoute("/bumps")} /> setRoute("/settings")} />
); } function Stat({ label, value }){ return (
{label}
{value}
); } function QuickAction({ icon, title, sub, onClick }){ return ( ); } // ============================================================ // Sales // ============================================================ function Sales(){ const { sales, setRoute, setModal } = useApp(); const [filter, setFilter] = React.useState("all"); const [search, setSearch] = React.useState(""); const filtered = sales.filter((s) => (filter === "all" || s.status === filter) && (s.id.toLowerCase().includes(search.toLowerCase()) || s.customer.toLowerCase().includes(search.toLowerCase()) || s.product.toLowerCase().includes(search.toLowerCase())) ); const openDetail = (s) => setModal({ title: s.id, sub: `${s.product} · ${fmtDateTime(s.date)}`, body: , foot: <> }); return ( Exportar CSV}>
{[ ["all", "Todas"], ["pago", "Pagas"], ["pendente", "Pendentes"], ["recusado", "Recusadas"], ["expirado", "Expiradas"], ].map(([k, l]) => ( ))}
setSearch(e.target.value)} style={{ paddingLeft: 36 }}/>
{filtered.map((s) => ( openDetail(s)}> ))} {filtered.length === 0 && ( )}
Pedido Cliente Produto Método Valor Status Data
{s.id}
{initials(s.customer)}
{s.customer}
{s.product} {s.method === "card" && s.installments > 1 && {s.installments}x} {fmtBRL(s.amount)} {fmtDateTime(s.date)}

Nenhuma venda encontrada

Ajuste o filtro ou pesquise por outro termo.
); } function SaleDetail({ sale }){ return (
Valor total
{fmtBRL(sale.amount)}
}/> {sale.method === "card" && } tx_98277{Math.floor(Math.random() * 999)}}/>
); } function Pair({ k, v }){ return (
{k} {v}
); } // ============================================================ // Customers — left sidebar groups them by status: // • Compraram → têm ao menos uma venda paga // • Não aprovou → só têm vendas recusadas / expiradas // • Pendentes → têm cobrança em andamento mas nenhuma paga ainda // ============================================================ function Customers(){ const { customers, sales } = useApp(); // Build a per-customer summary derived from the sales table const enriched = React.useMemo(() => customers.map((c) => { const list = sales.filter(s => s.customer === c.name); const paid = list.filter(s => s.status === "pago"); const declined = list.filter(s => s.status === "recusado" || s.status === "expirado"); const pending = list.filter(s => s.status === "pendente"); let group = "outros"; if (paid.length > 0) group = "comprou"; else if (pending.length > 0) group = "pendente"; else if (declined.length > 0) group = "recusado"; const lastSale = list.sort((a, b) => new Date(b.date) - new Date(a.date))[0]; return { ...c, sales: list, paid, declined, pending, group, lastDate: lastSale?.date }; }), [customers, sales]); const [group, setGroup] = React.useState("all"); const [search, setSearch] = React.useState(""); const filtered = enriched.filter(c => (group === "all" || c.group === group) && (c.name.toLowerCase().includes(search.toLowerCase()) || c.email.toLowerCase().includes(search.toLowerCase())) ); const [active, setActive] = React.useState(enriched[0]?.id); React.useEffect(() => { if (!filtered.find(c => c.id === active)) setActive(filtered[0]?.id); }, [group, search]); const cur = enriched.find(c => c.id === active) || enriched[0]; const counts = { all: enriched.length, comprou: enriched.filter(c => c.group === "comprou").length, pendente: enriched.filter(c => c.group === "pendente").length, recusado: enriched.filter(c => c.group === "recusado").length, }; const groupBadge = (g) => { if (g === "comprou") return ; if (g === "pendente") return ; if (g === "recusado") return Não aprovou; return Sem compras; }; return ( Filtros}>
{/* Left list with status tabs */}
setSearch(e.target.value)} style={{ paddingLeft: 36 }}/>
setGroup("all")} tone="mute"/> setGroup("comprou")} tone="ok"/> setGroup("pendente")} tone="warn"/> setGroup("recusado")} tone="bad"/>
{filtered.length === 0 && (

Nenhum cliente

Ajuste a busca ou troque a aba.
)} {filtered.map((c) => (
setActive(c.id)}>
{initials(c.name)}
{c.name} {c.email} {groupBadge(c.group)}
{fmtBRL(c.paid.reduce((a, s) => a + s.amount, 0))}
{c.paid.length} {c.paid.length === 1 ? "pedido" : "pedidos"}
))}
{/* Right detail */}
{cur && <>
{initials(cur.name)}

{cur.name}

{cur.email}
{groupBadge(cur.group)}
a + s.amount, 0))}/> 0 ? fmtBRL(cur.paid.reduce((a, s) => a + s.amount, 0) / cur.paid.length) : "—"}/>
{cur.lastDate && }

Histórico de cobranças

{cur.sales.length === 0 ? (

Nenhuma cobrança ainda

O cliente ainda não recebeu uma cobrança.
) : (
{cur.sales.map((s) => (
{s.id} {s.product} {fmtBRL(s.amount)}
))}
)}
}
); } function GroupTab({ label, count, active, onClick, tone }){ const toneColor = { ok: "var(--ok)", warn: "var(--warn)", bad: "var(--bad)", mute: "var(--graphite)", }[tone] || "var(--graphite)"; return ( ); } function KpiBlock({ label, value }){ return (
{label}
{value}
); } // ============================================================ // Settings // ============================================================ function Settings(){ const { pushToast, stripeEnabled, setStripeEnabled, pixel, setPixel, webhooks, setWebhooks } = useApp(); const [section, setSection] = React.useState("mp"); const [tokenVisible, setTokenVisible] = React.useState(false); const sections = [ { k: "mp", l: "Mercado Pago", icon: "money" }, { k: "stripe", l: "Stripe", icon: "card" }, { k: "webhook", l: "Webhook & notificações", icon: "bell" }, { k: "custom", l: "Webhooks personalizados", icon: "link" }, { k: "pixel", l: "Pixels & Tags", icon: "bolt" }, { k: "brand", l: "Branding", icon: "tag" }, { k: "urls", l: "URLs & domínio", icon: "globe" }, ]; return (
{sections.map((s) => (
setSection(s.k)} style={{ color: section === s.k ? "var(--ink)" : "var(--mute)", background: section === s.k ? "var(--paper)" : "transparent" }}> {s.l}
))}
{section === "mp" && (

Integração Mercado Pago

Configure o seu Access Token. Ele será armazenado e usado apenas no servidor.
Conectado
O token nunca é exposto no front-end. Toda chamada à API passa pelo backend.
)} {section === "stripe" && (

Integração Stripe

Pagamentos internacionais e cartões globais. Chave secreta protegida no servidor.
{stripeEnabled ? "Ativa no checkout" : "Não configurada"}
{stripeEnabled ? "Stripe ativa para clientes" : "Habilitar Stripe no checkout"}
Quando ativa, aparece como método na página de pagamento. Quando desligada, fica oculta para o cliente.
Esta chave pode ir ao front (usada no Stripe.js).
Permanece somente no servidor. Toda chamada Stripe passa pelo backend.

Métodos habilitados

)} {section === "webhook" && (

Webhook & notificações

Endpoint que o Mercado Pago chamará a cada evento.
Valide assinatura via header x-signature.

Push notifications

)} {section === "custom" && ( )} {section === "pixel" && ( )} {section === "brand" && (

Branding

Como o checkout aparece para o seu cliente.
)} {section === "urls" && (

URLs & domínio

Personalize o link público dos seus checkouts.
)}
); } function PushToggle({ label, defaultOn }){ const [on, setOn] = React.useState(!!defaultOn); return (
{label}
); } // ============================================================ // Pixels & Tags // ============================================================ function PixelSettings({ pixel, setPixel, pushToast }){ const update = (patch) => setPixel({ ...pixel, ...patch }); const test = () => { if (!pixel.fbPixelId){ pushToast("Informe um Pixel ID antes de testar.", "warn"); return; } if (window.__pagblackx){ window.__pagblackx.initPixel(pixel.fbPixelId); window.__pagblackx.trackPixel("PageView"); window.__pagblackx.trackPixel("InitiateCheckout", { value: 0, currency: "BRL", test: true }); } pushToast("Eventos de teste disparados para o Pixel."); }; return (

Pixels & Tags

Rastreie a jornada do cliente no Facebook / Meta Ads.
{pixel.enabled && pixel.fbPixelId ? "Rastreando" : "Inativo"}
{pixel.enabled ? "Facebook Pixel ativo" : "Habilitar Facebook Pixel"}
Quando ativo, eventos PageView, InitiateCheckout e Purchase são disparados automaticamente nas páginas do cliente.
update({ fbPixelId: e.target.value.replace(/[^0-9]/g, "") })}/>
Pegue no Gerenciador de Eventos da Meta. Apenas dígitos.

Eventos disparados

InitiateCheckout
quando o cliente abre a página de pagamento
Purchase
quando o pagamento é aprovado · envia value + currency
); } // ============================================================ // Webhooks personalizados — Make / Zapier / n8n / endpoint próprio // ============================================================ const WH_EVENTS = [ { k: "checkout.criado", l: "Checkout criado" }, { k: "pagamento.aprovado", l: "Pagamento aprovado" }, { k: "pagamento.recusado", l: "Pagamento recusado" }, { k: "pix.expirado", l: "Pix expirado" }, { k: "novo.cliente", l: "Novo cliente" }, ]; function CustomWebhooks({ webhooks, setWebhooks, pushToast }){ const [draft, setDraft] = React.useState({ name: "", url: "", event: "pagamento.aprovado" }); const custom = webhooks.filter(w => !w.system); const system = webhooks.filter(w => w.system); const add = () => { if (!draft.name.trim() || !/^https?:\/\//.test(draft.url)) { pushToast("Informe um nome e uma URL https válida.", "warn"); return; } setWebhooks([...webhooks, { id: uid(), active: true, system: false, ...draft }]); setDraft({ name: "", url: "", event: "pagamento.aprovado" }); pushToast("Webhook adicionado."); }; const toggle = (id) => setWebhooks(webhooks.map(w => w.id === id ? { ...w, active: !w.active } : w)); const del = (id) => { setWebhooks(webhooks.filter(w => w.id !== id)); pushToast("Webhook removido.", "bad"); }; const test = (w) => { const payload = { test: true, event: w.event, fired_at: new Date().toISOString() }; if (window.__pagblackx) window.__pagblackx.fireWebhook(w.url, payload); pushToast(`Teste enviado para ${w.name}.`); }; return (

Webhooks personalizados

Conecte ao Make, Zapier, n8n ou ao seu próprio endpoint. Dispara JSON em cada evento.
{custom.length} configurado{custom.length === 1 ? "" : "s"}
Adicionar novo webhook
setDraft({ ...draft, name: e.target.value })}/> setDraft({ ...draft, url: e.target.value })}/>
Sistema (Mercado Pago / Stripe)
{system.map((w) => (
{w.name} {w.url}
Ativo
))}
Personalizados ({custom.length})
{custom.length === 0 && (

Sem webhooks personalizados

Cole uma URL acima para receber JSON em cada evento.
)} {custom.map((w) => (
{w.name} {w.url} → {WH_EVENTS.find(e => e.k === w.event)?.l || w.event}
))}
); } Object.assign(window, { Shell, Sidebar, Topbar, Dashboard, Sales, Customers, Settings });