2026-06-07 22:30:33 +00:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
|
|
|
import { MetricsCard } from '@/components/dashboard/MetricsCard';
|
|
|
|
|
import { CampaignTable } from '@/components/dashboard/CampaignTable';
|
|
|
|
|
import { DateFilter } from '@/components/dashboard/DateFilter';
|
|
|
|
|
import { Spinner } from '@/components/ui/Spinner';
|
|
|
|
|
import { RefreshCw } from 'lucide-react';
|
|
|
|
|
import type { CampaignWithInsights, AccountOverview, DatePreset } from '@/lib/meta/types';
|
|
|
|
|
|
|
|
|
|
export default function DashboardPage() {
|
|
|
|
|
const [datePreset, setDatePreset] = useState<DatePreset>('last_30d');
|
|
|
|
|
const [campaigns, setCampaigns] = useState<CampaignWithInsights[]>([]);
|
|
|
|
|
const [overview, setOverview] = useState<AccountOverview | null>(null);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
|
const [error, setError] = useState('');
|
|
|
|
|
|
|
|
|
|
const fetchData = useCallback(async (showLoading = true) => {
|
|
|
|
|
if (showLoading) setLoading(true);
|
|
|
|
|
else setRefreshing(true);
|
|
|
|
|
setError('');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const [campaignsRes, overviewRes] = await Promise.all([
|
|
|
|
|
fetch(`/api/campaigns?date_preset=${datePreset}`),
|
|
|
|
|
fetch(`/api/campaigns/insights?date_preset=${datePreset}`),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (!campaignsRes.ok || !overviewRes.ok) {
|
|
|
|
|
throw new Error('Erro ao carregar dados');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const campaignsData = await campaignsRes.json();
|
|
|
|
|
const overviewData = await overviewRes.json();
|
|
|
|
|
|
|
|
|
|
if (campaignsData.success) setCampaigns(campaignsData.data);
|
|
|
|
|
if (overviewData.success) setOverview(overviewData.data);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Erro ao carregar dados');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
setRefreshing(false);
|
|
|
|
|
}
|
|
|
|
|
}, [datePreset]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchData();
|
|
|
|
|
}, [fetchData]);
|
|
|
|
|
|
|
|
|
|
// Auto-refresh every 60s
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const interval = setInterval(() => fetchData(false), 60000);
|
|
|
|
|
return () => clearInterval(interval);
|
|
|
|
|
}, [fetchData]);
|
|
|
|
|
|
|
|
|
|
const handleStatusChange = async (campaignId: string, newStatus: 'ACTIVE' | 'PAUSED') => {
|
|
|
|
|
const res = await fetch(`/api/campaigns/${campaignId}/status`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ status: newStatus }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!res.ok) throw new Error('Erro ao atualizar status');
|
|
|
|
|
|
|
|
|
|
// Update local state
|
|
|
|
|
setCampaigns(prev =>
|
|
|
|
|
prev.map(c =>
|
|
|
|
|
c.campaign.id === campaignId
|
|
|
|
|
? { ...c, campaign: { ...c.campaign, status: newStatus } }
|
|
|
|
|
: c
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBudgetChange = async (campaignId: string, newBudget: number) => {
|
|
|
|
|
const res = await fetch(`/api/campaigns/${campaignId}/budget`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ daily_budget: newBudget }),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!res.ok) throw new Error('Erro ao atualizar orçamento');
|
|
|
|
|
|
|
|
|
|
// Update local state
|
|
|
|
|
setCampaigns(prev =>
|
|
|
|
|
prev.map(c =>
|
|
|
|
|
c.campaign.id === campaignId
|
|
|
|
|
? { ...c, campaign: { ...c.campaign, daily_budget: String(newBudget) } }
|
|
|
|
|
: c
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatCurrency = (val: any) => `R$ ${Number(val || 0).toFixed(2)}`;
|
|
|
|
|
const formatNumber = (val: any) => Number(val || 0).toLocaleString('pt-BR');
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col gap-6">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-center justify-between animate-fade-in-up">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-2xl font-bold text-white">Dashboard</h1>
|
|
|
|
|
<p className="text-sm mt-1" style={{ color: 'var(--muted-fg)' }}>
|
|
|
|
|
Visão geral das suas campanhas de Facebook Ads
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<DateFilter selected={datePreset} onChange={setDatePreset} />
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => fetchData(false)}
|
|
|
|
|
disabled={refreshing}
|
|
|
|
|
className="btn-ghost flex items-center gap-2"
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw size={14} className={refreshing ? 'animate-spin-slow' : ''} />
|
|
|
|
|
Atualizar
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Error */}
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="px-5 py-4 rounded-xl animate-fade-in" style={{ background: 'rgba(239,68,68,0.08)', border: '1px solid rgba(239,68,68,0.15)' }}>
|
|
|
|
|
<p className="text-sm" style={{ color: '#f87171' }}>{error}</p>
|
|
|
|
|
<p className="text-xs mt-1" style={{ color: 'var(--muted-fg)' }}>
|
|
|
|
|
Verifique se as credenciais do Meta estão configuradas no .env.local
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Metrics Cards */}
|
|
|
|
|
{loading ? (
|
feat: posicionamentos restritos + stats completas + auditoria IA melhorada
- Posicionamentos: novas campanhas saem só com Feed+Stories+Reels (FB+IG),
sem Audience Network, Messenger, coluna direita ou Marketplace
- Botão "Corrigir Posicionamentos" (ícone Target) em cada ad set existente —
busca targeting atual, preserva geo/idade/interesses e aplica as restrições
- Stats: compras, alcance, CPA e reach nos cards do dashboard e na tabela
(campanha, ad set e anúncio); bug de leads/ROAS sempre 0 no nível campanha corrigido
- Auditoria IA: prompt ciente do objetivo (Leads/Vendas/Tráfego), passa todas
as métricas relevantes (CTR, CPC, CPL, CPA, alcance, impressões); hero da
tela mostra cards dinâmicos conforme o tipo de campanha
- strategist.ts: corrigido acesso indevido a actions/purchase_roas nos insights mapeados
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 17:34:47 +00:00
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
|
|
|
{Array.from({ length: 8 }).map((_, i) => (
|
2026-06-07 22:30:33 +00:00
|
|
|
<div key={i} className="shimmer h-[120px]" />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : overview ? (
|
feat: posicionamentos restritos + stats completas + auditoria IA melhorada
- Posicionamentos: novas campanhas saem só com Feed+Stories+Reels (FB+IG),
sem Audience Network, Messenger, coluna direita ou Marketplace
- Botão "Corrigir Posicionamentos" (ícone Target) em cada ad set existente —
busca targeting atual, preserva geo/idade/interesses e aplica as restrições
- Stats: compras, alcance, CPA e reach nos cards do dashboard e na tabela
(campanha, ad set e anúncio); bug de leads/ROAS sempre 0 no nível campanha corrigido
- Auditoria IA: prompt ciente do objetivo (Leads/Vendas/Tráfego), passa todas
as métricas relevantes (CTR, CPC, CPL, CPA, alcance, impressões); hero da
tela mostra cards dinâmicos conforme o tipo de campanha
- strategist.ts: corrigido acesso indevido a actions/purchase_roas nos insights mapeados
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 17:34:47 +00:00
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
2026-06-07 22:30:33 +00:00
|
|
|
<MetricsCard
|
|
|
|
|
title="Investido"
|
|
|
|
|
value={formatCurrency(overview.total_spend)}
|
|
|
|
|
subtitle={`${overview.campaigns_active} campanhas ativas`}
|
|
|
|
|
icon="dollar"
|
|
|
|
|
color="purple"
|
|
|
|
|
delay={1}
|
|
|
|
|
/>
|
|
|
|
|
<MetricsCard
|
feat: posicionamentos restritos + stats completas + auditoria IA melhorada
- Posicionamentos: novas campanhas saem só com Feed+Stories+Reels (FB+IG),
sem Audience Network, Messenger, coluna direita ou Marketplace
- Botão "Corrigir Posicionamentos" (ícone Target) em cada ad set existente —
busca targeting atual, preserva geo/idade/interesses e aplica as restrições
- Stats: compras, alcance, CPA e reach nos cards do dashboard e na tabela
(campanha, ad set e anúncio); bug de leads/ROAS sempre 0 no nível campanha corrigido
- Auditoria IA: prompt ciente do objetivo (Leads/Vendas/Tráfego), passa todas
as métricas relevantes (CTR, CPC, CPL, CPA, alcance, impressões); hero da
tela mostra cards dinâmicos conforme o tipo de campanha
- strategist.ts: corrigido acesso indevido a actions/purchase_roas nos insights mapeados
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 17:34:47 +00:00
|
|
|
title="Compras"
|
|
|
|
|
value={formatNumber(overview.total_purchases)}
|
|
|
|
|
subtitle={overview.avg_cpa > 0 ? `CPA: ${formatCurrency(overview.avg_cpa)}` : 'Custo por compra'}
|
|
|
|
|
icon="trending"
|
2026-06-07 22:30:33 +00:00
|
|
|
color="green"
|
|
|
|
|
delay={2}
|
|
|
|
|
/>
|
|
|
|
|
<MetricsCard
|
|
|
|
|
title="ROAS"
|
|
|
|
|
value={overview.total_roas > 0 ? `${overview.total_roas.toFixed(2)}x` : '—'}
|
|
|
|
|
subtitle="Retorno sobre investimento"
|
|
|
|
|
icon="trending"
|
|
|
|
|
color="blue"
|
|
|
|
|
delay={3}
|
|
|
|
|
/>
|
feat: posicionamentos restritos + stats completas + auditoria IA melhorada
- Posicionamentos: novas campanhas saem só com Feed+Stories+Reels (FB+IG),
sem Audience Network, Messenger, coluna direita ou Marketplace
- Botão "Corrigir Posicionamentos" (ícone Target) em cada ad set existente —
busca targeting atual, preserva geo/idade/interesses e aplica as restrições
- Stats: compras, alcance, CPA e reach nos cards do dashboard e na tabela
(campanha, ad set e anúncio); bug de leads/ROAS sempre 0 no nível campanha corrigido
- Auditoria IA: prompt ciente do objetivo (Leads/Vendas/Tráfego), passa todas
as métricas relevantes (CTR, CPC, CPL, CPA, alcance, impressões); hero da
tela mostra cards dinâmicos conforme o tipo de campanha
- strategist.ts: corrigido acesso indevido a actions/purchase_roas nos insights mapeados
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 17:34:47 +00:00
|
|
|
<MetricsCard
|
|
|
|
|
title="Leads"
|
|
|
|
|
value={formatNumber(overview.total_leads)}
|
|
|
|
|
subtitle={overview.total_leads > 0 ? `CPL: ${formatCurrency(overview.total_spend / overview.total_leads)}` : 'Custo por lead'}
|
|
|
|
|
icon="users"
|
|
|
|
|
color="amber"
|
|
|
|
|
delay={4}
|
|
|
|
|
/>
|
|
|
|
|
<MetricsCard
|
|
|
|
|
title="Alcance"
|
|
|
|
|
value={formatNumber(overview.total_reach)}
|
|
|
|
|
subtitle={`${formatNumber(overview.total_impressions)} impressões`}
|
|
|
|
|
icon="chart"
|
|
|
|
|
color="cyan"
|
|
|
|
|
delay={5}
|
|
|
|
|
/>
|
2026-06-07 22:30:33 +00:00
|
|
|
<MetricsCard
|
|
|
|
|
title="CTR"
|
|
|
|
|
value={`${overview.avg_ctr.toFixed(2)}%`}
|
|
|
|
|
subtitle={`${formatNumber(overview.total_clicks)} cliques`}
|
|
|
|
|
icon="target"
|
feat: posicionamentos restritos + stats completas + auditoria IA melhorada
- Posicionamentos: novas campanhas saem só com Feed+Stories+Reels (FB+IG),
sem Audience Network, Messenger, coluna direita ou Marketplace
- Botão "Corrigir Posicionamentos" (ícone Target) em cada ad set existente —
busca targeting atual, preserva geo/idade/interesses e aplica as restrições
- Stats: compras, alcance, CPA e reach nos cards do dashboard e na tabela
(campanha, ad set e anúncio); bug de leads/ROAS sempre 0 no nível campanha corrigido
- Auditoria IA: prompt ciente do objetivo (Leads/Vendas/Tráfego), passa todas
as métricas relevantes (CTR, CPC, CPL, CPA, alcance, impressões); hero da
tela mostra cards dinâmicos conforme o tipo de campanha
- strategist.ts: corrigido acesso indevido a actions/purchase_roas nos insights mapeados
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 17:34:47 +00:00
|
|
|
color="pink"
|
|
|
|
|
delay={6}
|
2026-06-07 22:30:33 +00:00
|
|
|
/>
|
|
|
|
|
<MetricsCard
|
|
|
|
|
title="CPC"
|
|
|
|
|
value={formatCurrency(overview.avg_cpc)}
|
|
|
|
|
subtitle="Custo por clique"
|
|
|
|
|
icon="click"
|
feat: posicionamentos restritos + stats completas + auditoria IA melhorada
- Posicionamentos: novas campanhas saem só com Feed+Stories+Reels (FB+IG),
sem Audience Network, Messenger, coluna direita ou Marketplace
- Botão "Corrigir Posicionamentos" (ícone Target) em cada ad set existente —
busca targeting atual, preserva geo/idade/interesses e aplica as restrições
- Stats: compras, alcance, CPA e reach nos cards do dashboard e na tabela
(campanha, ad set e anúncio); bug de leads/ROAS sempre 0 no nível campanha corrigido
- Auditoria IA: prompt ciente do objetivo (Leads/Vendas/Tráfego), passa todas
as métricas relevantes (CTR, CPC, CPL, CPA, alcance, impressões); hero da
tela mostra cards dinâmicos conforme o tipo de campanha
- strategist.ts: corrigido acesso indevido a actions/purchase_roas nos insights mapeados
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 17:34:47 +00:00
|
|
|
color="purple"
|
|
|
|
|
delay={7}
|
2026-06-07 22:30:33 +00:00
|
|
|
/>
|
|
|
|
|
<MetricsCard
|
|
|
|
|
title="CPM"
|
|
|
|
|
value={formatCurrency(overview.avg_cpm)}
|
feat: posicionamentos restritos + stats completas + auditoria IA melhorada
- Posicionamentos: novas campanhas saem só com Feed+Stories+Reels (FB+IG),
sem Audience Network, Messenger, coluna direita ou Marketplace
- Botão "Corrigir Posicionamentos" (ícone Target) em cada ad set existente —
busca targeting atual, preserva geo/idade/interesses e aplica as restrições
- Stats: compras, alcance, CPA e reach nos cards do dashboard e na tabela
(campanha, ad set e anúncio); bug de leads/ROAS sempre 0 no nível campanha corrigido
- Auditoria IA: prompt ciente do objetivo (Leads/Vendas/Tráfego), passa todas
as métricas relevantes (CTR, CPC, CPL, CPA, alcance, impressões); hero da
tela mostra cards dinâmicos conforme o tipo de campanha
- strategist.ts: corrigido acesso indevido a actions/purchase_roas nos insights mapeados
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 17:34:47 +00:00
|
|
|
subtitle="Custo por mil impressões"
|
2026-06-07 22:30:33 +00:00
|
|
|
icon="chart"
|
feat: posicionamentos restritos + stats completas + auditoria IA melhorada
- Posicionamentos: novas campanhas saem só com Feed+Stories+Reels (FB+IG),
sem Audience Network, Messenger, coluna direita ou Marketplace
- Botão "Corrigir Posicionamentos" (ícone Target) em cada ad set existente —
busca targeting atual, preserva geo/idade/interesses e aplica as restrições
- Stats: compras, alcance, CPA e reach nos cards do dashboard e na tabela
(campanha, ad set e anúncio); bug de leads/ROAS sempre 0 no nível campanha corrigido
- Auditoria IA: prompt ciente do objetivo (Leads/Vendas/Tráfego), passa todas
as métricas relevantes (CTR, CPC, CPL, CPA, alcance, impressões); hero da
tela mostra cards dinâmicos conforme o tipo de campanha
- strategist.ts: corrigido acesso indevido a actions/purchase_roas nos insights mapeados
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 17:34:47 +00:00
|
|
|
color="amber"
|
|
|
|
|
delay={8}
|
2026-06-07 22:30:33 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
{/* Campaigns Table */}
|
|
|
|
|
<CampaignTable
|
|
|
|
|
campaigns={campaigns}
|
|
|
|
|
loading={loading}
|
|
|
|
|
onStatusChange={handleStatusChange}
|
|
|
|
|
onBudgetChange={handleBudgetChange}
|
2026-06-08 15:54:00 +00:00
|
|
|
onAdCreated={() => fetchData(false)}
|
2026-06-07 22:30:33 +00:00
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
|
|
|
|
{!loading && (
|
|
|
|
|
<div className="flex items-center justify-between animate-fade-in">
|
|
|
|
|
<p className="text-xs" style={{ color: 'var(--muted-fg)' }}>
|
|
|
|
|
{campaigns.length} campanha{campaigns.length !== 1 ? 's' : ''} encontrada{campaigns.length !== 1 ? 's' : ''}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs" style={{ color: 'var(--muted-fg)' }}>
|
|
|
|
|
Auto-refresh: 60s • Dados via Meta Marketing API v20.0
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Loading overlay on refresh */}
|
|
|
|
|
{refreshing && (
|
|
|
|
|
<div className="fixed bottom-6 right-6 flex items-center gap-2 px-4 py-2.5 rounded-xl animate-fade-in" style={{ background: 'rgba(99,102,241,0.15)', border: '1px solid rgba(99,102,241,0.25)' }}>
|
|
|
|
|
<Spinner size="sm" className="text-[var(--accent)]" />
|
|
|
|
|
<span className="text-xs font-medium" style={{ color: 'var(--accent-soft-fg)' }}>Atualizando...</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|