fix(erp): enable erp pages and menu items
This commit is contained in:
@@ -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 há 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 há 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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user