metaads-pro/src/app/dashboard/activity/page.tsx

145 lines
6.4 KiB
TypeScript
Raw Normal View History

'use client';
import React, { useState, useEffect } from 'react';
import { Activity, CheckCircle, Clock, XCircle, Zap, Filter } from 'lucide-react';
import { Spinner } from '@/components/ui/Spinner';
interface LogEntry {
id: string;
timestamp: string;
campaign_name: string;
rule_name: string;
action: string;
details: string;
status: 'executed' | 'pending' | 'failed' | '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 [allLogs, setAllLogs] = useState<LogEntry[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
(async () => {
try {
const res = await fetch('/api/engine/logs');
const data = await res.json();
if (data.success) setAllLogs(data.data || []);
} catch {
/* ignora */
} finally {
setLoading(false);
}
})();
}, []);
const logs = filter === 'all' ? allLogs : allLogs.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: allLogs.length, color: 'var(--accent-soft-fg)' },
{ label: 'Executados', value: allLogs.filter((l) => l.status === 'executed').length, color: '#4ade80' },
{ label: 'Pendentes', value: allLogs.filter((l) => l.status === 'pending').length, color: '#fbbf24' },
{ label: 'Pulados', value: allLogs.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)' }}>
{loading ? (
<div className="p-12 flex justify-center">
<Spinner />
</div>
) : logs.length === 0 ? (
<div className="p-12 text-center">
<p className="text-[var(--muted-fg)]">Nenhuma atividade ainda. Quando o engine agir ou recomendar algo, aparece aqui.</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>
);
}