HEFESTO v1.0 - Sistema de Controle Orçamentário para Facilities
- Backend NestJS com 12 módulos - Frontend React com dashboard e gestão - Manuais técnico e de negócios (MD + PDF) - Workflow de aprovação com alçadas - RBAC com 6 perfis de acesso
This commit is contained in:
279
frontend/src/pages/Relatorios.tsx
Normal file
279
frontend/src/pages/Relatorios.tsx
Normal file
@@ -0,0 +1,279 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
BarChart3,
|
||||
Download,
|
||||
Calendar,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
PieChart,
|
||||
FileText,
|
||||
Filter
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
|
||||
LineChart, Line, PieChart as RechartsPie, Pie, Cell, Legend,
|
||||
AreaChart, Area
|
||||
} from 'recharts'
|
||||
|
||||
const monthlyData = [
|
||||
{ name: 'Jan', orcamento: 120, gasto: 95, economia: 25 },
|
||||
{ name: 'Fev', orcamento: 115, gasto: 110, economia: 5 },
|
||||
{ name: 'Mar', orcamento: 130, gasto: 105, economia: 25 },
|
||||
{ name: 'Abr', orcamento: 125, gasto: 120, economia: 5 },
|
||||
{ name: 'Mai', orcamento: 140, gasto: 115, economia: 25 },
|
||||
{ name: 'Jun', orcamento: 135, gasto: 125, economia: 10 },
|
||||
]
|
||||
|
||||
const categoryData = [
|
||||
{ name: 'Manutenção', value: 35, color: '#E65100' },
|
||||
{ name: 'Limpeza', value: 25, color: '#1A237E' },
|
||||
{ name: 'Segurança', value: 20, color: '#FF8F00' },
|
||||
{ name: 'Utilities', value: 12, color: '#2E7D32' },
|
||||
{ name: 'Outros', value: 8, color: '#757575' },
|
||||
]
|
||||
|
||||
const trendData = [
|
||||
{ name: 'Sem 1', demandas: 15, os: 12 },
|
||||
{ name: 'Sem 2', demandas: 22, os: 18 },
|
||||
{ name: 'Sem 3', demandas: 18, os: 16 },
|
||||
{ name: 'Sem 4', demandas: 25, os: 22 },
|
||||
]
|
||||
|
||||
const fornecedorData = [
|
||||
{ name: 'Tech Solutions', atendimentos: 45, satisfacao: 4.5 },
|
||||
{ name: 'EletroFix', atendimentos: 38, satisfacao: 4.8 },
|
||||
{ name: 'HidroServ', atendimentos: 32, satisfacao: 4.2 },
|
||||
{ name: 'CleanPro', atendimentos: 28, satisfacao: 4.6 },
|
||||
{ name: 'ElevaTech', atendimentos: 15, satisfacao: 3.9 },
|
||||
]
|
||||
|
||||
export default function Relatorios() {
|
||||
const [period, setPeriod] = useState('mensal')
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-text">Relatórios</h1>
|
||||
<p className="text-gray mt-1">Análises e métricas de facilities</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<select
|
||||
value={period}
|
||||
onChange={(e) => setPeriod(e.target.value)}
|
||||
className="input-field w-40"
|
||||
>
|
||||
<option value="semanal">Semanal</option>
|
||||
<option value="mensal">Mensal</option>
|
||||
<option value="trimestral">Trimestral</option>
|
||||
<option value="anual">Anual</option>
|
||||
</select>
|
||||
<button className="btn-primary flex items-center gap-2">
|
||||
<Download className="w-5 h-5" />
|
||||
Exportar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center">
|
||||
<TrendingUp className="w-5 h-5 text-primary" />
|
||||
</div>
|
||||
<span className="text-sm text-gray">Economia Total</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-text">95K</p>
|
||||
<p className="text-xs text-green-600 mt-1">+12% vs período anterior</p>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-lg bg-secondary/10 flex items-center justify-center">
|
||||
<FileText className="w-5 h-5 text-secondary" />
|
||||
</div>
|
||||
<span className="text-sm text-gray">Demandas</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-text">156</p>
|
||||
<p className="text-xs text-green-600 mt-1">+8% vs período anterior</p>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center">
|
||||
<BarChart3 className="w-5 h-5 text-accent" />
|
||||
</div>
|
||||
<span className="text-sm text-gray">OS Concluídas</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-text">142</p>
|
||||
<p className="text-xs text-green-600 mt-1">91% taxa de conclusão</p>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
|
||||
<TrendingDown className="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<span className="text-sm text-gray">Tempo Médio</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-text">3.2 dias</p>
|
||||
<p className="text-xs text-green-600 mt-1">-15% vs período anterior</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts Row 1 */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
{/* Bar Chart - Orçamento vs Gasto */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-text">Orçamento vs Gasto</h2>
|
||||
<p className="text-sm text-gray">Comparativo mensal (em milhares)</p>
|
||||
</div>
|
||||
<button className="p-2 rounded-lg hover:bg-gray-100 text-gray">
|
||||
<Filter className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-72">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={monthlyData} barGap={8}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#E0E0E0" vertical={false} />
|
||||
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#757575', fontSize: 12 }} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#757575', fontSize: 12 }} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #E0E0E0',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)'
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="orcamento" fill="#1A237E" radius={[4, 4, 0, 0]} name="Orçamento" />
|
||||
<Bar dataKey="gasto" fill="#E65100" radius={[4, 4, 0, 0]} name="Gasto" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pie Chart - Por Categoria */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-text">Gastos por Categoria</h2>
|
||||
<p className="text-sm text-gray">Distribuição percentual</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-72">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RechartsPie>
|
||||
<Pie
|
||||
data={categoryData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={100}
|
||||
paddingAngle={4}
|
||||
dataKey="value"
|
||||
>
|
||||
{categoryData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Legend
|
||||
verticalAlign="bottom"
|
||||
iconType="circle"
|
||||
iconSize={8}
|
||||
formatter={(value) => <span className="text-sm text-gray">{value}</span>}
|
||||
/>
|
||||
<Tooltip />
|
||||
</RechartsPie>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts Row 2 */}
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
{/* Area Chart - Tendência */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-text">Tendência Semanal</h2>
|
||||
<p className="text-sm text-gray">Demandas vs Ordens de Serviço</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={trendData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#E0E0E0" vertical={false} />
|
||||
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#757575', fontSize: 12 }} />
|
||||
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#757575', fontSize: 12 }} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #E0E0E0',
|
||||
borderRadius: '12px'
|
||||
}}
|
||||
/>
|
||||
<Area type="monotone" dataKey="demandas" stroke="#E65100" fill="#E65100" fillOpacity={0.2} name="Demandas" />
|
||||
<Area type="monotone" dataKey="os" stroke="#1A237E" fill="#1A237E" fillOpacity={0.2} name="Ordens de Serviço" />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bar Chart - Fornecedores */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-text">Top Fornecedores</h2>
|
||||
<p className="text-sm text-gray">Por número de atendimentos</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={fornecedorData} layout="vertical" barSize={20}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#E0E0E0" horizontal={false} />
|
||||
<XAxis type="number" axisLine={false} tickLine={false} tick={{ fill: '#757575', fontSize: 12 }} />
|
||||
<YAxis type="category" dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#757575', fontSize: 12 }} width={100} />
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #E0E0E0',
|
||||
borderRadius: '12px'
|
||||
}}
|
||||
/>
|
||||
<Bar dataKey="atendimentos" fill="#FF8F00" radius={[0, 4, 4, 0]} name="Atendimentos" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Reports */}
|
||||
<div className="card">
|
||||
<h2 className="text-lg font-semibold text-text mb-4">Relatórios Disponíveis</h2>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ name: 'Relatório Mensal', desc: 'Resumo completo do mês', icon: <Calendar className="w-5 h-5" /> },
|
||||
{ name: 'Análise de Custos', desc: 'Detalhamento por categoria', icon: <PieChart className="w-5 h-5" /> },
|
||||
{ name: 'Performance Fornecedores', desc: 'Avaliação e métricas', icon: <TrendingUp className="w-5 h-5" /> },
|
||||
{ name: 'Histórico de Demandas', desc: 'Todas as solicitações', icon: <FileText className="w-5 h-5" /> },
|
||||
].map((report, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className="flex items-center gap-4 p-4 bg-card rounded-xl hover:bg-gray-100 transition-all text-left"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
|
||||
{report.icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-text">{report.name}</p>
|
||||
<p className="text-xs text-gray">{report.desc}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user