152 lines
7 KiB
TypeScript
152 lines
7 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import React, { useState } from 'react';
|
||
|
|
import { Activity, CheckCircle, Clock, XCircle, Zap, Filter } from 'lucide-react';
|
||
|
|
|
||
|
|
interface LogEntry {
|
||
|
|
id: string;
|
||
|
|
timestamp: string;
|
||
|
|
campaign_name: string;
|
||
|
|
rule_name: string;
|
||
|
|
action: string;
|
||
|
|
details: string;
|
||
|
|
status: 'executed' | 'pending' | 'failed' | 'skipped';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Demo data (will be replaced with Supabase query)
|
||
|
|
const DEMO_LOGS: LogEntry[] = [
|
||
|
|
{
|
||
|
|
id: '1', timestamp: new Date(Date.now() - 3600000).toISOString(),
|
||
|
|
campaign_name: 'Campanha Conversão Q1', rule_name: 'CPL Alto — Pausar',
|
||
|
|
action: 'pause', details: 'CPL = R$67.30 (threshold: R$50.00)', status: 'executed',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '2', timestamp: new Date(Date.now() - 7200000).toISOString(),
|
||
|
|
campaign_name: 'Remarketing Visitantes', rule_name: 'ROAS Baixo — Pausar',
|
||
|
|
action: 'pause', details: 'ROAS = 0.32x (threshold: 0.50x)', status: 'executed',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '3', timestamp: new Date(Date.now() - 14400000).toISOString(),
|
||
|
|
campaign_name: 'Tráfego Blog', rule_name: 'CTR Muito Baixo — Pausar',
|
||
|
|
action: 'pause', details: 'CTR = 0.21% (threshold: 0.50%)', status: 'executed',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '4', timestamp: new Date(Date.now() - 28800000).toISOString(),
|
||
|
|
campaign_name: 'Leads Premium', rule_name: 'ROAS Excelente — Aumentar Budget',
|
||
|
|
action: 'increase_budget', details: 'Budget +20%: R$50.00 → R$60.00. ROAS = 4.2x', status: 'executed',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: '5', timestamp: new Date(Date.now() - 43200000).toISOString(),
|
||
|
|
campaign_name: 'Campanha Vídeo', rule_name: 'CPC Alto — Reduzir Budget',
|
||
|
|
action: 'reduce_budget', details: 'Em cooldown (48h)', status: 'skipped',
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
const statusConfig = {
|
||
|
|
executed: { icon: <CheckCircle size={14} />, color: '#4ade80', bg: 'rgba(34,197,94,0.1)', label: 'Executado' },
|
||
|
|
pending: { icon: <Clock size={14} />, color: '#fbbf24', bg: 'rgba(245,158,11,0.1)', label: 'Pendente' },
|
||
|
|
failed: { icon: <XCircle size={14} />, color: '#f87171', bg: 'rgba(239,68,68,0.1)', label: 'Falhou' },
|
||
|
|
skipped: { icon: <Clock size={14} />, color: '#8888a0', bg: 'var(--overlay-05)', label: 'Pulado' },
|
||
|
|
};
|
||
|
|
|
||
|
|
function timeAgo(dateStr: string): string {
|
||
|
|
const diff = Date.now() - new Date(dateStr).getTime();
|
||
|
|
const mins = Math.floor(diff / 60000);
|
||
|
|
if (mins < 60) return `${mins}min atrás`;
|
||
|
|
const hours = Math.floor(mins / 60);
|
||
|
|
if (hours < 24) return `${hours}h atrás`;
|
||
|
|
const days = Math.floor(hours / 24);
|
||
|
|
return `${days}d atrás`;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function ActivityPage() {
|
||
|
|
const [filter, setFilter] = useState<string>('all');
|
||
|
|
const logs = filter === 'all' ? DEMO_LOGS : DEMO_LOGS.filter((l) => l.status === filter);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex flex-col gap-6">
|
||
|
|
{/* Header */}
|
||
|
|
<div className="flex items-center justify-between animate-fade-in-up">
|
||
|
|
<div className="flex items-center gap-3">
|
||
|
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ background: 'rgba(34,197,94,0.15)', border: '1px solid rgba(34,197,94,0.25)' }}>
|
||
|
|
<Activity size={20} style={{ color: '#4ade80' }} />
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<h1 className="text-2xl font-bold text-white">Atividade</h1>
|
||
|
|
<p className="text-sm" style={{ color: 'var(--muted-fg)' }}>Ações executadas pelo agente de automação</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-1 p-1 rounded-xl" style={{ background: 'var(--overlay-03)', border: '1px solid var(--overlay-06)' }}>
|
||
|
|
<Filter size={14} className="ml-2" style={{ color: 'var(--muted-fg)' }} />
|
||
|
|
{['all', 'executed', 'pending', 'skipped', 'failed'].map((f) => (
|
||
|
|
<button key={f} onClick={() => setFilter(f)} className="px-3 py-1.5 rounded-lg text-xs font-medium transition-all"
|
||
|
|
style={{ background: filter === f ? 'var(--accent)' : 'transparent', color: filter === f ? 'white' : 'var(--muted-fg)' }}>
|
||
|
|
{f === 'all' ? 'Todos' : statusConfig[f as keyof typeof statusConfig].label}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Stats */}
|
||
|
|
<div className="grid grid-cols-4 gap-4 animate-fade-in-up delay-1">
|
||
|
|
{[
|
||
|
|
{ label: 'Total', value: DEMO_LOGS.length, color: 'var(--accent-soft-fg)' },
|
||
|
|
{ label: 'Executados', value: DEMO_LOGS.filter((l) => l.status === 'executed').length, color: '#4ade80' },
|
||
|
|
{ label: 'Pendentes', value: DEMO_LOGS.filter((l) => l.status === 'pending').length, color: '#fbbf24' },
|
||
|
|
{ label: 'Pulados', value: DEMO_LOGS.filter((l) => l.status === 'skipped').length, color: '#8888a0' },
|
||
|
|
].map((s) => (
|
||
|
|
<div key={s.label} className="glass-card-static p-4 text-center">
|
||
|
|
<p className="text-2xl font-bold" style={{ color: s.color }}>{s.value}</p>
|
||
|
|
<p className="text-xs mt-1" style={{ color: 'var(--muted-fg)' }}>{s.label}</p>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Timeline */}
|
||
|
|
<div className="glass-card-static overflow-hidden animate-fade-in-up delay-2">
|
||
|
|
<div className="p-5 border-b" style={{ borderColor: 'var(--overlay-06)' }}>
|
||
|
|
<h2 className="text-base font-semibold text-white">Timeline</h2>
|
||
|
|
</div>
|
||
|
|
<div className="divide-y" style={{ borderColor: 'var(--overlay-04)' }}>
|
||
|
|
{logs.length === 0 ? (
|
||
|
|
<div className="p-12 text-center">
|
||
|
|
<p className="text-[var(--muted-fg)]">Nenhuma atividade encontrada</p>
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
logs.map((log) => {
|
||
|
|
const sc = statusConfig[log.status];
|
||
|
|
return (
|
||
|
|
<div key={log.id} className="flex items-start gap-4 p-5 hover:bg-white/[0.02] transition-colors">
|
||
|
|
{/* Timeline dot */}
|
||
|
|
<div className="mt-1 flex flex-col items-center">
|
||
|
|
<div className="w-8 h-8 rounded-full flex items-center justify-center" style={{ background: sc.bg }}>
|
||
|
|
<Zap size={14} style={{ color: sc.color }} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{/* Content */}
|
||
|
|
<div className="flex-1 min-w-0">
|
||
|
|
<div className="flex items-center gap-2 mb-1">
|
||
|
|
<p className="font-medium text-white text-sm">{log.campaign_name}</p>
|
||
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium" style={{ background: sc.bg, color: sc.color }}>
|
||
|
|
{sc.icon} {sc.label}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
<p className="text-xs" style={{ color: 'var(--muted-fg)' }}>
|
||
|
|
Regra: <span className="text-white">{log.rule_name}</span>
|
||
|
|
</p>
|
||
|
|
<p className="text-xs mt-1" style={{ color: 'var(--muted-fg)' }}>{log.details}</p>
|
||
|
|
</div>
|
||
|
|
{/* Time */}
|
||
|
|
<span className="text-xs whitespace-nowrap" style={{ color: 'var(--muted-fg)' }}>
|
||
|
|
{timeAgo(log.timestamp)}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|