331 lines
14 KiB
JavaScript
331 lines
14 KiB
JavaScript
import React from 'react';
|
|
import {
|
|
Users,
|
|
FileText,
|
|
ArrowUpRight,
|
|
ArrowDownRight,
|
|
DollarSign,
|
|
Activity,
|
|
Loader2,
|
|
Download
|
|
} from 'lucide-react';
|
|
import {
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
AreaChart,
|
|
Area,
|
|
PieChart,
|
|
Pie,
|
|
Cell
|
|
} from 'recharts';
|
|
import { motion } from 'framer-motion';
|
|
import { cn } from '@/lib/utils';
|
|
import { useCnabDashboard } from '../hooks/useCnabDashboard';
|
|
import RemessaDetailsModal from '../components/RemessaDetailsModal';
|
|
import { useCnabStore } from '../hooks/useCnabStore';
|
|
import CnabExcelTable from '../components/CnabExcelTable';
|
|
|
|
const COLORS = ['#4ade80', '#fbbf24', '#3b82f6', '#a855f7', '#ec4899'];
|
|
|
|
/**
|
|
* Cartão de Estatística (KPI) Premium
|
|
*/
|
|
const StatCard = ({ title, value, icon: Icon, trend, detail }) => (
|
|
<motion.div
|
|
whileHover={{ y: -5 }}
|
|
className="bg-white dark:bg-[#1b1b1b] border border-zinc-200 dark:border-zinc-800 rounded-3xl p-6 shadow-sm flex flex-col justify-between group hover:border-emerald-500/30 transition-all relative overflow-hidden"
|
|
>
|
|
{/* Background Decorative Element */}
|
|
<div className="absolute -right-4 -bottom-4 opacity-[0.03] dark:opacity-[0.05] group-hover:scale-110 transition-transform duration-700">
|
|
<Icon size={120} />
|
|
</div>
|
|
|
|
<div className="flex justify-between items-start mb-4">
|
|
<div className="p-3 bg-emerald-50 dark:bg-emerald-500/10 rounded-2xl text-emerald-600 dark:text-emerald-500 border border-emerald-100 dark:border-emerald-500/20 shadow-inner">
|
|
<Icon size={24} />
|
|
</div>
|
|
{trend && (
|
|
<div className={cn(
|
|
"flex items-center gap-1 px-2 py-1 rounded-full text-[10px] font-black",
|
|
trend > 0 ? "bg-emerald-500/10 text-emerald-500" : "bg-rose-500/10 text-rose-500"
|
|
)}>
|
|
{trend > 0 ? <ArrowUpRight size={12} /> : <ArrowDownRight size={12} />}
|
|
{Math.abs(trend)}%
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="text-zinc-500 dark:text-zinc-500 text-xs font-black uppercase tracking-widest mb-1">{title}</h3>
|
|
<p className="text-2xl font-black text-slate-900 dark:text-white tracking-tighter truncate">{value}</p>
|
|
<p className="text-[10px] text-zinc-400 dark:text-zinc-600 font-medium mt-1">{detail}</p>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
|
|
/**
|
|
* Dashboard principal do módulo CNAB
|
|
*/
|
|
const DashboardView = () => {
|
|
const { paymentMode } = useCnabStore();
|
|
const {
|
|
data,
|
|
loading,
|
|
selectedRemessa,
|
|
setSelectedRemessa,
|
|
remessaDetails,
|
|
loadingDetails,
|
|
fetchRemessaDetails,
|
|
exportRemessa
|
|
} = useCnabDashboard();
|
|
|
|
if (loading || !data) {
|
|
return (
|
|
<div className="h-full flex flex-col items-center justify-center space-y-4 animate-in fade-in duration-700">
|
|
<div className="relative">
|
|
<div className="w-16 h-16 rounded-3xl bg-emerald-500/10 border border-emerald-500/20 animate-spin flex items-center justify-center">
|
|
<Loader2 className="text-emerald-500 w-8 h-8" />
|
|
</div>
|
|
</div>
|
|
<p className="text-zinc-600 text-xs font-black uppercase tracking-[0.3em]">Sincronizando Dados...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const ICON_MAP = {
|
|
'Total Favorecidos': Users,
|
|
'Valor Total': DollarSign,
|
|
'Qtd. Remessas': FileText,
|
|
};
|
|
|
|
// Colunas para a tabela de remessas recentes
|
|
const remessaColumns = [
|
|
{ field: 'id', header: 'Nº Remessa', width: '120px', render: (row) => <span className="font-bold text-emerald-500">{row.id}</span> },
|
|
{ field: 'qtd', header: 'Quantidade', width: '120px' },
|
|
{ field: 'valor', header: 'Total', width: '150px', render: (row) => <span className="font-mono font-bold text-white">{row.valor}</span> },
|
|
{ field: 'data', header: 'Data Pagamento', width: '150px' },
|
|
{ field: 'status', header: 'Status', width: '120px', render: (row) => (
|
|
<span className="px-2 py-0.5 rounded text-[10px] font-black uppercase bg-emerald-500/10 text-emerald-500 border border-emerald-500/20">
|
|
{row.status}
|
|
</span>
|
|
)},
|
|
{
|
|
field: 'id',
|
|
header: 'Ações',
|
|
width: '120px',
|
|
render: (row) => (
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => exportRemessa(row.id)}
|
|
className="p-1.5 hover:bg-zinc-800 rounded transition-colors"
|
|
title="arquivo cnab"
|
|
>
|
|
<FileText size={16} className="text-zinc-500 hover:text-white" />
|
|
</button>
|
|
<button
|
|
onClick={() => fetchRemessaDetails(row.id)}
|
|
className="p-1.5 hover:bg-zinc-800 rounded transition-colors"
|
|
title="detalhamento cnab"
|
|
>
|
|
<DollarSign size={16} className="text-zinc-500 hover:text-emerald-500" />
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700 h-full flex flex-col overflow-auto custom-scrollbar pr-2 pb-10">
|
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-6 shrink-0">
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<div className="p-2 bg-emerald-500/10 rounded-lg text-emerald-500">
|
|
<Activity size={20} />
|
|
</div>
|
|
<h2 className="text-3xl font-black text-slate-900 dark:text-white tracking-tighter">Controle de <span className="text-emerald-500">Remessas</span></h2>
|
|
</div>
|
|
<p className="text-zinc-500 text-sm font-medium">Monitoramento em tempo real de arquivos e favorecidos em modo <span className="text-emerald-500 font-bold">{paymentMode}</span></p>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<button className="group flex items-center gap-2 px-5 py-2.5 bg-white dark:bg-zinc-900 hover:bg-zinc-50 dark:hover:bg-zinc-800 text-zinc-600 dark:text-zinc-400 rounded-2xl text-[10px] font-black uppercase tracking-widest transition-all border border-zinc-200 dark:border-zinc-800 shadow-sm active:scale-95">
|
|
<Activity size={16} className="group-hover:text-emerald-500 transition-colors" />
|
|
Análise Gráfica
|
|
</button>
|
|
<button className="group flex items-center gap-2 px-5 py-2.5 bg-white dark:bg-zinc-900 hover:bg-zinc-50 dark:hover:bg-zinc-800 text-zinc-600 dark:text-zinc-400 rounded-2xl text-[10px] font-black uppercase tracking-widest transition-all border border-zinc-200 dark:border-zinc-800 shadow-sm active:scale-95">
|
|
<FileText size={16} className="group-hover:text-amber-500 transition-colors" />
|
|
Relatórios
|
|
</button>
|
|
<button className="group flex items-center gap-2 px-6 py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white rounded-2xl text-[10px] font-black uppercase tracking-widest transition-all shadow-xl shadow-emerald-900/20 active:scale-95 border border-emerald-500/20">
|
|
<Download size={16} />
|
|
Exportar Painel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 shrink-0">
|
|
{data.stats.map((stat, idx) => (
|
|
<StatCard
|
|
key={idx}
|
|
{...stat}
|
|
icon={ICON_MAP[stat.title] || Activity}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Charts Section */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 min-h-[400px]">
|
|
{/* Main Bar Chart */}
|
|
<div className="bg-white dark:bg-[#1b1b1b] border border-zinc-200 dark:border-zinc-800 rounded-[2.5rem] p-8 shadow-sm flex flex-col group overflow-hidden relative">
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-emerald-500/5 blur-3xl rounded-full" />
|
|
|
|
<div className="flex justify-between items-center mb-8 relative">
|
|
<h3 className="text-lg font-bold text-slate-900 dark:text-white flex items-center gap-2">
|
|
<DollarSign size={18} className="text-emerald-500" />
|
|
Volume Financeiro Mensal
|
|
</h3>
|
|
<select className="bg-zinc-100 dark:bg-zinc-900 border-none rounded-lg text-xs font-bold text-zinc-500 px-3 py-1.5 outline-none cursor-pointer">
|
|
<option>Últimos 6 meses</option>
|
|
<option>2025</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex-1 min-h-[300px]">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<AreaChart data={data.chartData}>
|
|
<defs>
|
|
<linearGradient id="colorValue" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3}/>
|
|
<stop offset="95%" stopColor="#10b981" stopOpacity={0}/>
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" dark:stroke="#2a2a2a" vertical={false} />
|
|
<XAxis
|
|
dataKey="name"
|
|
stroke="#64748b"
|
|
fontSize={10}
|
|
fontWeight="bold"
|
|
tickLine={false}
|
|
axisLine={false}
|
|
dy={10}
|
|
/>
|
|
<YAxis
|
|
stroke="#64748b"
|
|
fontSize={10}
|
|
fontWeight="bold"
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickFormatter={(value) => `R$ ${(value / 1000000).toFixed(1)}M`}
|
|
/>
|
|
<Tooltip
|
|
contentStyle={{ backgroundColor: '#18181b', border: '1px solid #27272a', borderRadius: '12px', color: '#fff' }}
|
|
itemStyle={{ color: '#10b981' }}
|
|
cursor={{stroke: '#10b981', strokeWidth: 1}}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="valor"
|
|
stroke="#10b981"
|
|
strokeWidth={3}
|
|
fillOpacity={1}
|
|
fill="url(#colorValue)"
|
|
animationDuration={2000}
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Pie Chart */}
|
|
<div className="bg-white dark:bg-[#1b1b1b] border border-zinc-200 dark:border-zinc-800 rounded-[2.5rem] p-8 shadow-sm flex flex-col relative overflow-hidden">
|
|
<div className="absolute bottom-0 left-0 w-32 h-32 bg-emerald-500/5 blur-3xl rounded-full" />
|
|
|
|
<h3 className="text-lg font-bold text-slate-900 dark:text-white mb-8 flex items-center gap-2 relative">
|
|
<Activity size={18} className="text-emerald-500" />
|
|
Distribuição por Modalidade
|
|
</h3>
|
|
|
|
<div className="flex-1 min-h-[300px] flex flex-col md:flex-row items-center justify-center gap-8">
|
|
<div className="flex-1 w-full">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<PieChart>
|
|
<Pie
|
|
data={data.pieData}
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={70}
|
|
outerRadius={100}
|
|
paddingAngle={8}
|
|
dataKey="value"
|
|
animationDuration={2500}
|
|
>
|
|
{data.pieData.map((entry, index) => (
|
|
<Cell
|
|
key={`cell-${index}`}
|
|
fill={COLORS[index % COLORS.length]}
|
|
stroke="rgba(0,0,0,0)"
|
|
strokeWidth={0}
|
|
/>
|
|
))}
|
|
</Pie>
|
|
<Tooltip
|
|
contentStyle={{ backgroundColor: '#18181b', border: '1px solid #27272a', borderRadius: '12px', color: '#fff' }}
|
|
/>
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
|
|
{/* Legend */}
|
|
<div className="flex flex-col gap-3 relative">
|
|
{data.pieData.map((item, idx) => (
|
|
<div key={idx} className="flex items-center gap-3">
|
|
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: COLORS[idx % COLORS.length] }} />
|
|
<div>
|
|
<p className="text-[10px] text-zinc-500 font-black uppercase tracking-widest leading-none">{item.name}</p>
|
|
<p className="text-sm font-bold text-white leading-tight">{item.value}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recent Remessas Table Area */}
|
|
<div className="flex flex-col flex-1 min-h-[400px]">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-lg font-bold text-slate-900 dark:text-white tracking-tight flex items-center gap-2">
|
|
<FileText size={18} className="text-emerald-500" />
|
|
Arquivos Gerados Recentes
|
|
</h3>
|
|
<button className="text-xs font-bold text-zinc-500 hover:text-emerald-500 transition-colors uppercase tracking-widest">Ver todos arquivos</button>
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<CnabExcelTable
|
|
data={data.recentRemessas}
|
|
columns={remessaColumns}
|
|
pageSize={5}
|
|
rowKey="id"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Details Modal */}
|
|
<RemessaDetailsModal
|
|
isOpen={!!selectedRemessa}
|
|
onClose={() => setSelectedRemessa(null)}
|
|
remessaId={selectedRemessa}
|
|
details={remessaDetails}
|
|
loading={loadingDetails}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DashboardView;
|