eubottura's picture
Upload pages/index.js with huggingface_hub
92aa266 verified
import { useState, useEffect } from 'react';
import {
BarChart3,
RefreshCw,
Download,
Settings,
Sun,
Moon,
TrendingUp,
DollarSign,
Users,
ShoppingCart,
AlertCircle,
CheckCircle2,
XCircle,
ArrowUpDown,
ArrowUp,
ArrowDown,
Clock,
Trophy
} from 'lucide-react';
// --- Types & Interfaces ---
interface CustomAttribute {
key: string;
value: string;
}
interface Order {
id: string;
name: string;
createdAt: string;
displayFinancialStatus: string;
totalPriceSet: {
shopMoney: {
amount: string;
};
};
customer: {
email: string | null;
} | null;
customAttributes: CustomAttribute[];
}
interface UTMData {
utmContent: string;
totalPedidos: number;
pedidosPagos: number;
pedidosPendentes: number;
clientesUnicos: number;
totalVendas: number;
vendasPagas: number;
taxaPagamento: number;
}
interface OrderSnapshot {
timestamp: string;
orderIds: string[];
}
interface NewOrdersByCampaign {
utmContent: string;
newOrders: number;
totalValue: number;
paidOrders: number;
paidValue: number;
}
interface NewOrdersReport {
newOrderCount: number;
newOrdersTotal: number;
newOrdersPaid: number;
newOrdersPaidCount: number;
newOrderNumbers: string[];
timeDifference: string;
campaignBreakdown: NewOrdersByCampaign[];
}
type SortColumn = 'utmContent' | 'lastParameter' | 'totalPedidos' | 'pedidosPagos' | 'pedidosPendentes' | 'clientesUnicos' | 'totalVendas' | 'vendasPagas' | 'taxaPagamento';
type SortDirection = 'asc' | 'desc';
// --- Helper Functions ---
const formatCurrency = (value: number): string => {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
};
const formatPercentage = (value: number): string => {
return `${value.toFixed(1)}%`;
};
const extractLastParameter = (utmContent: string): string => {
if (utmContent === 'Sem UTM Content') return 'N/A';
const matches = utmContent.match(/\[([^\]]+)\]/g);
if (!matches || matches.length === 0) return utmContent.substring(0, 20) + (utmContent.length > 20 ? '...' : '');
const lastMatch = matches[matches.length - 1];
return lastMatch.replace(/[\[\]]/g, '').trim();
};
// --- Main Component ---
export default function AnalyticsDashboard() {
// -- State --
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
const [configOpen, setConfigOpen] = useState(false);
// Shopify Config
const [storeName, setStoreName] = useState('');
const [accessToken, setAccessToken] = useState('');
const [isConfigured, setIsConfigured] = useState(false);
// App State
const [dateRangeOption, setDateRangeOption] = useState<string>('hoje');
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [utmData, setUtmData] = useState<UTMData[]>([]);
const [totalOrders, setTotalOrders] = useState<number>(0);
const [lastUpdate, setLastUpdate] = useState<string>('');
const [sortColumn, setSortColumn] = useState<SortColumn>('totalVendas');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
const [snapshot, setSnapshot] = useState<OrderSnapshot | null>(null);
const [newOrdersReport, setNewOrdersReport] = useState<NewOrdersReport | null>(null);
const [isFirstLoad, setIsFirstLoad] = useState<boolean>(true);
const [hasLoadedData, setHasLoadedData] = useState<boolean>(false);
const [dayChanged, setDayChanged] = useState<boolean>(false);
// -- Effects --
useEffect(() => {
const savedStore = localStorage.getItem('shopify_store');
const savedToken = localStorage.getItem('shopify_token');
const savedTheme = localStorage.getItem('theme') as 'dark' | 'light' | null;
if (savedStore) setStoreName(savedStore);
if (savedToken) setAccessToken(savedToken);
if (savedStore && savedToken) setIsConfigured(true);
if (savedTheme) {
setTheme(savedTheme);
document.documentElement.classList.toggle('dark', savedTheme === 'dark');
}
}, []);
const toggleTheme = () => {
const newTheme = theme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
document.documentElement.classList.toggle('dark', newTheme === 'dark');
};
// -- Logic --
const getDateRange = (): { startDate: string; endDate: string } => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
let startDate: Date;
let endDate: Date = new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1);
switch (dateRangeOption) {
case 'hoje': startDate = today; break;
case 'ontem':
startDate = new Date(today.getTime() - 24 * 60 * 60 * 1000);
endDate = new Date(today.getTime() - 1);
break;
case 'ultimos7': startDate = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000); break;
case 'ultimos30': startDate = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000); break;
default: startDate = today;
}
return {
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
};
};
const normalizeStoreName = (name: string): string => {
return name.replace(/^https?:\/\//, '').replace(/\.myshopify\.com.*$/, '').split('/')[0];
};
const fetchOrders = async () => {
if (!storeName || !accessToken) {
setError('Por favor, configure as credenciais da Shopify primeiro.');
setConfigOpen(true);
return;
}
setLoading(true);
setError(null);
const normalizedStore = normalizeStoreName(storeName);
const { startDate, endDate } = getDateRange();
const allOrders: Order[] = [];
let hasNextPage = true;
let cursor: string | null = null;
const ordersQuery = `query GetOrders($first: Int!, $after: String, $query: String!) {
orders(first: $first, after: $after, query: $query, sortKey: CREATED_AT) {
edges {
node {
id
name
createdAt
displayFinancialStatus
totalPriceSet { shopMoney { amount } }
customer { email }
customAttributes { key value }
}
}
pageInfo { hasNextPage endCursor }
}
}`;
try {
while (hasNextPage) {
const response = await fetch(`https://${normalizedStore}.myshopify.com/admin/api/2023-10/graphql.json`, {
method: 'POST',
headers: {
'X-Shopify-Access-Token': accessToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: ordersQuery,
variables: {
first: 250,
after: cursor,
query: `created_at:>='${startDate}' created_at:<='${endDate}'`,
},
}),
});
const data = await response.json();
if (data.errors) {
throw new Error(data.errors.map((e: any) => e.message).join(', '));
}
if (data?.data?.orders?.edges) {
const orders = data.data.orders.edges.map((edge: any) => edge.node);
allOrders.push(...orders);
hasNextPage = data.data.orders.pageInfo.hasNextPage;
cursor = data.data.orders.pageInfo.endCursor;
} else {
hasNextPage = false;
}
}
// Process Data
const report = analyzeNewOrders(allOrders, snapshot);
setNewOrdersReport(report);
setSnapshot(createSnapshot(allOrders));
setIsFirstLoad(false);
setHasLoadedData(true);
setDayChanged(false);
processOrders(allOrders);
setTotalOrders(allOrders.length);
const now = new Date();
setLastUpdate(now.toLocaleString('pt-BR'));
} catch (err: any) {
setError(err.message || 'Erro ao carregar pedidos.');
} finally {
setLoading(false);
}
};
const createSnapshot = (orders: Order[]): OrderSnapshot => ({
timestamp: new Date().toISOString(),
orderIds: orders.map(o => o.id),
});
const analyzeNewOrders = (currentOrders: Order[], previousSnapshot: OrderSnapshot | null) => {
if (!previousSnapshot) return null;
const previousOrderIds = new Set(previousSnapshot.orderIds);
const newOrders = currentOrders.filter(order => !previousOrderIds.has(order.id));
if (newOrders.length === 0) {
return {
newOrderCount: 0,
newOrdersTotal: 0,
newOrdersPaid: 0,
newOrdersPaidCount: 0,
newOrderNumbers: [],
timeDifference: calculateTimeDifference(previousSnapshot.timestamp),
campaignBreakdown: [],
};
}
const newOrdersTotal = newOrders.reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount || '0'), 0);
const paidOrders = newOrders.filter(o => o.displayFinancialStatus === 'PAID');
const newOrdersPaid = paidOrders.reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount || '0'), 0);
// Group by campaign
const campaignGroups = new Map<string, Order[]>();
newOrders.forEach(order => {
let utmContent = 'Sem UTM Content';
const attr = order.customAttributes?.find(a => a.key === 'utm_content');
if (attr?.value) utmContent = attr.value.trim();
if (!campaignGroups.has(utmContent)) campaignGroups.set(utmContent, []);
campaignGroups.get(utmContent)!.push(order);
});
const campaignBreakdown: NewOrdersByCampaign[] = [];
campaignGroups.forEach((orders, utmContent) => {
const totalValue = orders.reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount || '0'), 0);
const paid = orders.filter(o => o.displayFinancialStatus === 'PAID');
const paidValue = paid.reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount || '0'), 0);
campaignBreakdown.push({ utmContent, newOrders: orders.length, totalValue, paidOrders: paid.length, paidValue });
});
return {
newOrderCount: newOrders.length,
newOrdersTotal,
newOrdersPaid,
newOrdersPaidCount: paidOrders.length,
newOrderNumbers: newOrders.map(o => o.name),
timeDifference: calculateTimeDifference(previousSnapshot.timestamp),
campaignBreakdown,
};
};
const calculateTimeDifference = (timestamp: string): string => {
const diffMs = new Date().getTime() - new Date(timestamp).getTime();
const diffMins = Math.floor(diffMs / 60000);
if (diffMins < 1) return 'menos de 1 minuto';
const hours = Math.floor(diffMins / 60);
const mins = diffMins % 60;
return hours === 0 ? `${mins}m` : `${hours}h e ${mins}m`;
};
const processOrders = (orders: Order[]) => {
const utmGroups = new Map<string, Order[]>();
orders.forEach(order => {
let utmContent = 'Sem UTM Content';
const attr = order.customAttributes?.find(a => a.key === 'utm_content');
if (attr?.value) utmContent = attr.value.trim();
if (!utmGroups.has(utmContent)) utmGroups.set(utmContent, []);
utmGroups.get(utmContent)!.push(order);
});
const processed: UTMData[] = [];
utmGroups.forEach((group, utmContent) => {
const totalPedidos = group.length;
const pedidosPagos = group.filter(o => o.displayFinancialStatus === 'PAID').length;
const uniqueEmails = new Set(group.map(o => o.customer?.email || `no-email-${o.id}`));
const totalVendas = group.reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount || '0'), 0);
const vendasPagas = group.filter(o => o.displayFinancialStatus === 'PAID').reduce((sum, o) => sum + parseFloat(o.totalPriceSet.shopMoney.amount || '0'), 0);
processed.push({
utmContent,
totalPedidos,
pedidosPagos,
pedidosPendentes: totalPedidos - pedidosPagos,
clientesUnicos: uniqueEmails.size,
totalVendas,
vendasPagas,
taxaPagamento: totalPedidos > 0 ? (pedidosPagos / totalPedidos) * 100 : 0,
});
});
setUtmData(processed);
};
const saveConfig = () => {
localStorage.setItem('shopify_store', storeName);
localStorage.setItem('shopify_token', accessToken);
setIsConfigured(!!storeName && !!accessToken);
setConfigOpen(false);
};
const resetSnapshot = () => {
setSnapshot(null);
setNewOrdersReport(null);
setIsFirstLoad(true);
setHasLoadedData(false);
setUtmData([]);
setTotalOrders(0);
};
// -- Render Helpers --
const getSortedData = () => {
return [...utmData].sort((a, b) => {
let valA: any, valB: any;
if (sortColumn === 'lastParameter') {
valA = extractLastParameter(a.utmContent).toLowerCase();
valB = extractLastParameter(b.utmContent).toLowerCase();
} else if (sortColumn === 'utmContent') {
valA = a.utmContent.toLowerCase();
valB = b.utmContent.toLowerCase();
} else {
valA = a[sortColumn];
valB = b[sortColumn];
}
if (valA < valB) return sortDirection === 'asc' ? -1 : 1;
if (valA > valB) return sortDirection === 'asc' ? 1 : -1;
return 0;
});
};
const calculateTotals = (): UTMData => {
const totals: UTMData = {
utmContent: 'TOTAL GERAL',
totalPedidos: 0, pedidosPagos: 0, pedidosPendentes: 0,
clientesUnicos: totalOrders, totalVendas: 0, vendasPagas: 0, taxaPagamento: 0
};
utmData.forEach(row => {
totals.totalPedidos += row.totalPedidos;
totals.pedidosPagos += row.pedidosPagos;
totals.pedidosPendentes += row.pedidosPendentes;
totals.totalVendas += row.totalVendas;
totals.vendasPagas += row.vendasPagas;
});
totals.taxaPagamento = totals.totalPedidos > 0 ? (totals.pedidosPagos / totals.totalPedidos) * 100 : 0;
return totals;
};
const generateCSV = () => {
const headers = ['UTM Content', 'Total Pedidos', 'Pedidos Pagos', 'Pendentes', 'Clientes Únicos', 'Total Vendas', 'Vendas Pagas', 'Taxa (%)'];
const rows = getSortedData().map(r => [
r.utmContent, r.totalPedidos, r.pedidosPagos, r.pedidosPendentes,
r.clientesUnicos, r.totalVendas.toFixed(2), r.vendasPagas.toFixed(2), r.taxaPagamento.toFixed(1)
]);
const totals = calculateTotals();
rows.push([totals.utmContent, ...Object.values(totals).slice(1).map(v => typeof v === 'number' ? v.toFixed(v % 1 === 0 ? 0 : 2) : v)]);
const csvContent = [headers, ...rows].map(r => r.join(',')).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `relatorio-utm-${new Date().toISOString().split('T')[0]}.csv`;
link.click();
};
const sortedData = getSortedData();
const totals = calculateTotals();
const top3Performers = [...utmData]
.filter(r => r.utmContent !== 'Sem UTM Content')
.sort((a, b) => b.taxaPagamento - a.taxaPagamento)
.slice(0, 3);
return (
<div className={`min-h-screen transition-colors duration-200 ${theme === 'dark' ? 'bg-slate-950 text-slate-100' : 'bg-gray-50 text-slate-900'}`}>
{/* Header */}
<header className={`border-b ${theme === 'dark' ? 'border-slate-800 bg-slate-900/50 backdrop-blur' : 'border-gray-200 bg-white/80 backdrop-blur'} sticky top-0 z-50`}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-indigo-600 rounded-lg">
<BarChart3 className="w-5 h-5 text-white" />
</div>
<h1 className="text-xl font-bold tracking-tight">UTM Analytics Pro</h1>
</div>
<div className="flex items-center gap-4">
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noopener noreferrer" className="text-xs font-medium text-indigo-500 hover:text-indigo-400 transition-colors">
Built with anycoder
</a>
<button onClick={() => setConfigOpen(!configOpen)} className={`p-2 rounded-lg transition-colors ${theme === 'dark' ? 'hover:bg-slate-800 text-slate-400' : 'hover:bg-gray-100 text-gray-500'}`}>
<Settings className="w-5 h-5" />
</button>
<button onClick={toggleTheme} className={`p-2 rounded-lg transition-colors ${theme === 'dark' ? 'hover:bg-slate-800 text-yellow-400' : 'hover:bg-gray-100 text-slate-600'}`}>
{theme === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
</button>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
{/* Config Panel */}
{configOpen && (
<div className={`rounded-xl border ${theme === 'dark' ? 'bg-slate-900 border-slate-800' : 'bg-white border-gray-200'} p-6 shadow-sm animate-in fade-in slide-in-from-top-4 duration-300`}>
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Settings className="w-5 h-5 text-indigo-500" />
Configuração da Shopify
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1.5">Nome da Loja (subdomínio)</label>
<input
type="text"
placeholder="ex: minha-loja"
value={storeName}
onChange={(e) => setStoreName(e.target.value)}
className={`w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all ${theme === 'dark' ? 'bg-slate-950 border-slate-700 text-white placeholder-slate-500' : 'bg-gray-50 border-gray-300 text-gray-900'}`}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1.5">Token de Acesso (Admin API)</label>
<input
type="password"
placeholder="shpat_xxxxx..."
value={accessToken}
onChange={(e) => setAccessToken(e.target.value)}
className={`w-full px-4 py-2 rounded-lg border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all ${theme === 'dark' ? 'bg-slate-950 border-slate-700 text-white placeholder-slate-500' : 'bg-gray-50 border-gray-300 text-gray-900'}`}
/>
</div>
</div>
<div className="mt-4 flex justify-end">
<button onClick={saveConfig} className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors">
Salvar Configurações
</button>
</div>
</div>
)}
{!isConfigured && !configOpen && (
<div className={`rounded-xl border border-dashed p-8 text-center ${theme === 'dark' ? 'border-slate-700 bg-slate-900/30' : 'border-gray-300 bg-gray-50'}`}>
<AlertCircle className="w-12 h-12 text-indigo-500 mx-auto mb-3" />
<h3 className="text-lg font-medium mb-2">Configure sua integração</h3>
<p className={`text-sm mb-4 ${theme === 'dark' ? 'text-slate-400' : 'text-gray-500'}`}>Insira suas credenciais da Shopify para começar a analisar os pedidos.</p>
<button onClick={() => setConfigOpen(true)} className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors">
Configurar Agora
</button>
</div>
)}
{isConfigured && (
<>
{/* Controls */}
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
<div className="flex flex-wrap gap-2">
{['hoje', 'ontem', 'ultimos7', 'ultimos30'].map((opt) => (
<button
key={opt}
onClick={() => setDateRangeOption(opt)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-all capitalize ${
dateRangeOption === opt
? 'bg-indigo-600 text-white shadow-lg shadow-indigo-500/20'
: `${theme === 'dark' ? 'bg-slate-900 text-slate-400 hover:bg-slate-800 hover:text-slate-200 border border-slate-800' : 'bg-white text-gray-600 hover:bg-gray-100 border border-gray-200'}`
}`}
>
{opt === 'hoje' ? 'Hoje' : opt === 'ontem' ? 'Ontem' : opt === 'ultimos7' ? '7 Dias' : '30 Dias'}
</button>
))}
</div>
<div className="flex gap-2">
<button
onClick={fetchOrders}
disabled={loading}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-70 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors shadow-lg shadow-indigo-500/20"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
Atualizar
</button>
<button
onClick={generateCSV}
disabled={utmData.length === 0}
className={`flex items-center gap-2 px-4 py-2 font-medium rounded-lg transition-colors border ${utmData.length === 0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-opacity-80'} ${theme === 'dark' ? 'bg-slate-800 border-slate-700 text-white' : 'bg-white border-gray-200 text-gray-700'}`}
>
<Download className="w-4 h-4" />
Exportar
</button>
</div>
</div>
{error && (
<div className="p-4 bg-red-500/10 border border-red-500/50 rounded-lg text-red-500 flex items-start gap-3">
<XCircle className="w-5 h-5 mt-0.5 flex-shrink-0" />
<div>
<strong>Erro ao carregar dados</strong>
<p className="text-sm opacity-90 mt-1">{error}</p>
</div>
</div>
)}
{/* New Orders Alert */}
{hasLoadedData && !isFirstLoad && newOrdersReport && newOrdersReport.newOrderCount > 0 && (
<div className={`p-6 rounded-xl border-l-4 shadow-lg bg-gradient-to-r ${theme === 'dark' ? 'from-slate-900 to-slate-900 border-indigo-500' : 'from-white to-blue-50 border-indigo-600'}`}>
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-bold flex items-center gap-2">
<span className="bg-indigo-500 text-white p-1.5 rounded-lg">
<TrendingUp className="w-5 h-5" />
</span>
Novas Vendas Detectadas
</h3>
<p className={`text-sm mt-1 ${theme === 'dark' ? 'text-slate-400' : 'text-gray-500'}`}>
Desde a última atualização ({newOrdersReport.timeDifference})
</p>
</div>
<button onClick={resetSnapshot} className={`text-xs px-3 py-1.5 rounded-full border ${theme === 'dark' ? 'border-slate-700 hover:bg-slate-800' : 'border-gray-200 hover:bg-gray-100'}`}>
Resetar Contador
</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div>
<p className={`text-xs uppercase font-semibold tracking-wider ${theme === 'dark' ? 'text-slate-500' : 'text-gray-400'}`}>Pedidos</p>
<p className="text-2xl font-bold text-white">{newOrdersReport.newOrderCount}</p>
</div>
<div>
<p className={`text-xs uppercase font-semibold tracking-wider ${theme === 'dark' ? 'text-slate-500' : 'text-gray-400'}`}>Total Bruto</p>
<p className="text-2xl font-bold text-emerald-400">{formatCurrency(newOrdersReport.newOrdersTotal)}</p>
</div>
<div>
<p className={`text-xs uppercase font-semibold tracking-wider ${theme === 'dark' ? 'text-slate-500' : 'text-gray-400'}`}>Pagos</p>
<p className="text-2xl font-bold text-emerald-400">{formatCurrency(newOrdersReport.newOrdersPaid)}</p>
</div>
<div>
<p className={`text-xs uppercase font-semibold tracking-wider ${theme === 'dark' ? 'text-slate-500' : 'text-gray-400'}`}>Pedidos Pagos</p>
<p className="text-2xl font-bold text-white">{newOrdersReport.newOrdersPaidCount}</p>
</div>
</div>
{newOrdersReport.campaignBreakdown.length > 0 && (
<div>
<h4 className={`text-sm font-semibold mb-3 ${theme === 'dark' ? 'text-slate-300' : 'text-gray-700'}`">Detalhe por Campanha</h4>
<div className="space-y-2">
{newOrdersReport.campaignBreakdown.map((c) => (
<div key={c.utmContent} className={`flex items-center justify-between p-3 rounded-lg ${theme === 'dark' ? 'bg-slate-800/50' : 'bg-white border border-gray-100'}`}>
<div className="flex items-center gap-3">
<span className={`px-2 py-1 rounded text-xs font-bold ${c.paidOrders === c.newOrders ? 'bg-emerald-500/20 text-emerald-400' : 'bg-amber-500/20 text-amber-400'}`}>
{c.paidOrders}/{c.newOrders}
</span>
<span className="font-medium text-sm">{extractLastParameter(c.utmContent)}</span>
</div>
<span className="font-bold text-sm text-emerald-400">{formatCurrency(c.totalValue)}</span>
</div>
))}
</div>
</div>
)}
</div>
)}
{/* KPI Cards */}
{hasLoadedData && utmData.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[
{ label: 'Total Pedidos', value: totals.totalPedidos, icon: ShoppingCart, color: 'text-blue-500', bg: 'bg-blue-500/10' },
{ label: 'Receita Total', value: formatCurrency(totals.totalVendas), icon: DollarSign, color: 'text-emerald-400', bg: 'bg-emerald-500/10' },
{ label: 'Receita Paga', value: formatCurrency(totals.vendasPagas), icon: CheckCircle2, color: 'text-emerald-500', bg: 'bg-emerald-500/10' },
{ label: 'Taxa Pagamento', value: formatPercentage(totals.taxaPagamento), icon: TrendingUp, color: 'text-indigo-400', bg: 'bg-indigo-500/10' },
].map((kpi, idx) => (
<div key={idx} className={`p-6 rounded-xl border ${theme === 'dark' ? 'bg-slate-900 border-slate-800' : 'bg-white border-gray-200'} shadow-sm relative overflow-hidden group`}>
<div className={`absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity`}>
<kpi.icon className="w-16 h-16" />
</div>
<div className={`p-3 rounded-lg ${kpi.bg} ${kpi.color} mb-4 w-fit`}>
<kpi.icon className="w-6 h-6" />
</div>
<p className={`text-sm font-medium ${theme === 'dark