fix(erp): enable erp pages and menu items

This commit is contained in:
Erik Silva
2025-12-29 17:23:59 -03:00
parent e124a64a5d
commit adbff9bb1e
13990 changed files with 1110936 additions and 59 deletions

View File

@@ -1,16 +1,315 @@
'use client';
import React, { useState, useEffect } from 'react';
import {
AreaChart, Area, PieChart, Pie, Cell, ResponsiveContainer, CartesianGrid, XAxis, YAxis, Tooltip, Legend
} from 'recharts';
import {
ArrowTrendingUpIcon,
ArrowTrendingDownIcon,
CubeIcon,
CurrencyDollarIcon,
CreditCardIcon,
ClockIcon,
} from "@heroicons/react/24/outline";
import { erpApi, FinancialTransaction, Order, FinancialCategory, Entity } from '@/lib/api-erp';
import { formatCurrency } from '@/lib/format';
import { PageHeader, StatsCard, Card } from "@/components/ui";
import { SolutionGuard } from '@/components/auth/SolutionGuard';
const COLORS = ['#8b5cf6', '#ec4899', '#f43f5e', '#f59e0b', '#10b981', '#3b82f6'];
function ERPDashboardContent() {
const [transactions, setTransactions] = useState<FinancialTransaction[]>([]);
const [orders, setOrders] = useState<Order[]>([]);
const [categories, setCategories] = useState<FinancialCategory[]>([]);
const [entities, setEntities] = useState<Entity[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const [txData, orderData, categoriesData, entitiesData] = await Promise.all([
erpApi.getTransactions(),
erpApi.getOrders(),
erpApi.getFinancialCategories(),
erpApi.getEntities()
]);
setTransactions(Array.isArray(txData) ? txData : []);
setOrders(Array.isArray(orderData) ? orderData : []);
setCategories(Array.isArray(categoriesData) ? categoriesData : []);
setEntities(Array.isArray(entitiesData) ? entitiesData : []);
} catch (error) {
console.error('Error fetching dashboard data:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
const paidTransactions = (transactions || []).filter(t => t.status === 'paid');
const totalIncome = paidTransactions
.filter(t => t.type === 'income')
.reduce((sum, t) => sum + Number(t.amount || 0), 0);
const totalExpense = paidTransactions
.filter(t => t.type === 'expense')
.reduce((sum, t) => sum + Number(t.amount || 0), 0);
const pendingIncome = (transactions || [])
.filter(t => t.type === 'income' && t.status === 'pending')
.reduce((sum, t) => sum + Number(t.amount || 0), 0);
const pendingExpense = (transactions || [])
.filter(t => t.type === 'expense' && t.status === 'pending')
.reduce((sum, t) => sum + Number(t.amount || 0), 0);
const balance = totalIncome - totalExpense;
// Process chart data (Income vs Expense by Month)
const getChartData = () => {
const months = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'];
const currentYear = new Date().getFullYear();
const data = months.map((month, index) => {
const monthTransactions = paidTransactions.filter(t => {
const date = new Date(t.payment_date || t.due_date || '');
return date.getMonth() === index && date.getFullYear() === currentYear;
});
const income = monthTransactions
.filter(t => t.type === 'income')
.reduce((sum, t) => sum + Number(t.amount || 0), 0);
const expense = monthTransactions
.filter(t => t.type === 'expense')
.reduce((sum, t) => sum + Number(t.amount || 0), 0);
return { name: month, income, expense };
});
const currentMonthIndex = new Date().getMonth();
// Mostrar pelo menos os últimos 6 meses ou o ano todo se for o caso
return data.slice(Math.max(0, currentMonthIndex - 5), currentMonthIndex + 1);
};
// Process category data (Expenses by Category)
const getCategoryData = () => {
const expenseTransactions = paidTransactions.filter(t => t.type === 'expense');
const breakdown: Record<string, number> = {};
expenseTransactions.forEach(t => {
const category = categories.find(c => c.id === t.category_id)?.name || 'Outros';
breakdown[category] = (breakdown[category] || 0) + Number(t.amount || 0);
});
return Object.entries(breakdown)
.map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value)
.slice(0, 6);
};
const chartData = getChartData();
const categoryData = getCategoryData();
if (loading) return (
<div className="p-6 max-w-[1600px] mx-auto">
<div className="flex items-center justify-center h-[600px]">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500"></div>
</div>
</div>
);
return (
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
<PageHeader
title="Dashboard ERP"
description="Visão geral financeira e operacional em tempo real"
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatsCard
title="Receitas pagas"
value={formatCurrency(totalIncome)}
icon={<ArrowTrendingUpIcon className="w-6 h-6 text-emerald-500" />}
trend={{ value: formatCurrency(pendingIncome), label: 'pendente', type: 'up' }}
/>
<StatsCard
title="Despesas pagas"
value={formatCurrency(totalExpense)}
icon={<ArrowTrendingDownIcon className="w-6 h-6 text-rose-500" />}
trend={{ value: formatCurrency(pendingExpense), label: 'pendente', type: 'down' }}
/>
<StatsCard
title="Saldo em Caixa"
value={formatCurrency(balance)}
icon={<CurrencyDollarIcon className="w-6 h-6 text-brand-500" />}
/>
<StatsCard
title="Pedidos (Mês)"
value={(orders?.length || 0).toString()}
icon={<CubeIcon className="w-6 h-6 text-purple-500" />}
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<Card title="Evolução Financeira" description="Diferença entre entradas e saídas pagas nos últimos meses.">
<div className="h-[350px] w-full mt-4">
{chartData.some(d => d.income > 0 || d.expense > 0) ? (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
<defs>
<linearGradient id="colorIncome" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.1} />
<stop offset="95%" stopColor="#10b981" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorExpense" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.1} />
<stop offset="95%" stopColor="#ef4444" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#88888820" />
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: '#888' }} />
<YAxis axisLine={false} tickLine={false} tick={{ fontSize: 12, fill: '#888' }} tickFormatter={(val) => `R$${val}`} />
<Tooltip
contentStyle={{
borderRadius: '16px',
border: 'none',
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)',
backgroundColor: 'rgba(255, 255, 255, 0.9)'
}}
formatter={(value: any) => formatCurrency(value || 0)}
/>
<Legend verticalAlign="top" height={36} />
<Area
name="Receitas"
type="monotone"
dataKey="income"
stroke="#10b981"
fillOpacity={1}
fill="url(#colorIncome)"
strokeWidth={3}
/>
<Area
name="Despesas"
type="monotone"
dataKey="expense"
stroke="#ef4444"
fillOpacity={1}
fill="url(#colorExpense)"
strokeWidth={3}
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-full text-zinc-400 text-sm italic">
Ainda não dados financeiros suficientes para exibir o gráfico.
</div>
)}
</div>
</Card>
</div>
<div className="lg:col-span-1">
<Card title="Despesas por Categoria" description="Distribuição dos gastos pagos.">
<div className="h-[350px] w-full mt-4">
{categoryData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={categoryData}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={5}
dataKey="value"
>
{categoryData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip
contentStyle={{
borderRadius: '16px',
border: 'none',
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)'
}}
formatter={(value: any) => formatCurrency(value || 0)}
/>
<Legend />
</PieChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-full text-zinc-400 text-sm italic">
Ainda não despesas pagas registradas.
</div>
)}
</div>
</Card>
</div>
<div className="lg:col-span-3">
<Card title="Transações Recentes" description="Últimos lançamentos financeiros registrados no sistema.">
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-zinc-100 dark:border-zinc-800">
<th className="py-4 font-semibold text-zinc-900 dark:text-white">Descrição</th>
<th className="py-4 font-semibold text-zinc-900 dark:text-white">Categoria</th>
<th className="py-4 font-semibold text-zinc-900 dark:text-white">Data</th>
<th className="py-4 font-semibold text-zinc-900 dark:text-white text-right">Valor</th>
<th className="py-4 font-semibold text-zinc-900 dark:text-white text-right">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-50 dark:divide-zinc-900">
{transactions.slice(0, 5).map((t) => (
<tr key={t.id} className="hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors">
<td className="py-4 text-zinc-600 dark:text-zinc-400">{t.description}</td>
<td className="py-4 text-zinc-600 dark:text-zinc-400">
{categories.find(c => c.id === t.category_id)?.name || 'Outros'}
</td>
<td className="py-4 text-zinc-600 dark:text-zinc-400">
{new Date(t.payment_date || t.due_date || '').toLocaleDateString('pt-BR')}
</td>
<td className={`py-4 text-right font-medium ${t.type === 'income' ? 'text-emerald-600' : 'text-rose-600'}`}>
{t.type === 'income' ? '+' : '-'} {formatCurrency(t.amount)}
</td>
<td className="py-4 text-right">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${t.status === 'paid' ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-400' :
t.status === 'pending' ? 'bg-amber-100 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400' :
'bg-zinc-100 text-zinc-700 dark:bg-zinc-900/20 dark:text-zinc-400'
}`}>
{t.status === 'paid' ? 'Pago' : t.status === 'pending' ? 'Pendente' : 'Cancelado'}
</span>
</td>
</tr>
))}
{transactions.length === 0 && (
<tr>
<td colSpan={5} className="py-8 text-center text-zinc-400 italic">
Nenhuma transação encontrada.
</td>
</tr>
)}
</tbody>
</table>
</div>
</Card>
</div>
</div>
</div>
);
}
export default function ERPPage() {
return (
<SolutionGuard requiredSolution="erp">
<div className="p-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">ERP</h1>
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-8 text-center">
<p className="text-gray-500">Sistema Integrado de Gestão Empresarial em breve</p>
</div>
</div>
<ERPDashboardContent />
</SolutionGuard>
);
}