/* ============================================================
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 (
);
}
// ============================================================
// 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)}%`} />
{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 (
);
}
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 }}/>
| Pedido |
Cliente |
Produto |
Método |
Valor |
Status |
Data |
{filtered.map((s) => (
openDetail(s)}>
| {s.id} |
{initials(s.customer)}
{s.customer}
|
{s.product} |
{s.method === "card" && s.installments > 1 && {s.installments}x} |
{fmtBRL(s.amount)} |
|
{fmtDateTime(s.date)} |
))}
{filtered.length === 0 && (
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 */}
{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 && <>
{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 (
);
}
// ============================================================
// 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
)}
{section === "stripe" && (
Integração Stripe
Pagamentos internacionais e cartões globais. Chave secreta protegida no servidor.
{stripeEnabled ? "Ativa no checkout" : "Não configurada"}
)}
{section === "webhook" && (
Webhook & notificações
Endpoint que o Mercado Pago chamará a cada evento.
)}
{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"}
);
}
// ============================================================
// 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"}
Sistema (Mercado Pago / Stripe)
{system.map((w) => (
))}
Personalizados ({custom.length})
{custom.length === 0 && (
Sem webhooks personalizados
Cole uma URL acima para receber JSON em cada evento.
)}
{custom.map((w) => (
))}
);
}
Object.assign(window, { Shell, Sidebar, Topbar, Dashboard, Sales, Customers, Settings });