Major update: ESG, KPIs, metas, alertas, auditoria, documentos, importação, relatórios, subcategorias, dashboard orçamentos

This commit is contained in:
bigtux
2026-02-10 18:52:52 -03:00
parent d8ca580acb
commit 90c7a2cacb
92 changed files with 10265 additions and 1238 deletions

View File

@@ -1,278 +1,235 @@
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'
import { useState, useEffect } from 'react'
import { FileText, Loader2, Download, Calendar, TrendingUp, Building2, ClipboardList, Leaf } from 'lucide-react'
import api from '../services/api'
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 },
]
type TabKey = 'orcamento' | 'demandas' | 'fornecedores' | 'os' | 'esg_impacto' | 'esg_fornecedores' | 'esg_preventiva' | 'esg_governanca'
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 },
const tabs: { key: TabKey; label: string; icon: React.ReactNode; esg?: boolean }[] = [
{ key: 'orcamento', label: 'Orçamento', icon: <TrendingUp className="w-4 h-4" /> },
{ key: 'demandas', label: 'Demandas', icon: <FileText className="w-4 h-4" /> },
{ key: 'fornecedores', label: 'Fornecedores', icon: <Building2 className="w-4 h-4" /> },
{ key: 'os', label: 'OS', icon: <ClipboardList className="w-4 h-4" /> },
{ key: 'esg_impacto', label: 'Impacto Ambiental', icon: <Leaf className="w-4 h-4" />, esg: true },
{ key: 'esg_fornecedores', label: 'ESG Fornecedores', icon: <Leaf className="w-4 h-4" />, esg: true },
{ key: 'esg_preventiva', label: 'Evolução Preventiva', icon: <Leaf className="w-4 h-4" />, esg: true },
{ key: 'esg_governanca', label: 'Exceções Governança', icon: <Leaf className="w-4 h-4" />, esg: true },
]
export default function Relatorios() {
const [period, setPeriod] = useState('mensal')
const [activeTab, setActiveTab] = useState<TabKey>('orcamento')
const [loading, setLoading] = useState(true)
const [data, setData] = useState<any>(null)
const [dateFrom, setDateFrom] = useState('')
const [dateTo, setDateTo] = useState('')
useEffect(() => { fetchData() }, [activeTab, dateFrom, dateTo])
const endpoints: Record<TabKey, string> = {
orcamento: '/relatorios/orcamento-mensal',
demandas: '/relatorios/demandas-periodo',
fornecedores: '/relatorios/fornecedores-ranking',
os: '/relatorios/os-performance',
esg_impacto: '/relatorios/esg-impacto-ambiental',
esg_fornecedores: '/relatorios/esg-fornecedores',
esg_preventiva: '/relatorios/esg-evolucao-preventiva',
esg_governanca: '/relatorios/esg-excecoes-governanca',
}
const fetchData = async () => {
setLoading(true)
try {
const params: any = {}
if (dateFrom) params.data_inicio = dateFrom
if (dateTo) params.data_fim = dateTo
const { data } = await api.get(endpoints[activeTab], { params })
setData(data)
} catch (err) {
console.error('Error fetching report:', err)
setData(null)
} finally {
setLoading(false)
}
}
const renderEsgImpacto = () => {
if (!data) return null
return (
<div className="space-y-4">
{data.resumo && (
<div className="grid grid-cols-3 gap-4 mb-4">
{data.resumo.map((r: any) => (
<div key={r.impacto} className={`p-4 rounded-xl ${r.impacto === 'Alto' ? 'bg-red-50' : r.impacto === 'Médio' ? 'bg-amber-50' : 'bg-green-50'}`}>
<p className="text-sm text-gray">Impacto {r.impacto}</p>
<p className="text-2xl font-bold">{r.total}</p>
</div>
))}
</div>
)}
{renderGenericTable(data.detalhes || [])}
</div>
)
}
const renderEsgFornecedores = () => {
if (!data) return null
return (
<div className="space-y-4">
{data.resumo && (
<div className="grid grid-cols-3 gap-4 mb-4">
{data.resumo.map((r: any) => (
<div key={r.classificacao_esg} className={`p-4 rounded-xl ${r.classificacao_esg === 'Avançado' ? 'bg-green-50' : r.classificacao_esg === 'Intermediário' ? 'bg-amber-50' : 'bg-red-50'}`}>
<p className="text-sm text-gray">{r.classificacao_esg}</p>
<p className="text-2xl font-bold">{r.total}</p>
</div>
))}
</div>
)}
{data.fornecedores && (
<div className="overflow-x-auto">
<table className="w-full">
<thead><tr className="table-header">
<th className="table-cell">Fornecedor</th>
<th className="table-cell">ESG</th>
<th className="table-cell">Rating</th>
<th className="table-cell">Pol. Ambiental</th>
<th className="table-cell">SST</th>
<th className="table-cell">EPI</th>
<th className="table-cell">Treinada</th>
<th className="table-cell">Total OS</th>
</tr></thead>
<tbody>
{data.fornecedores.map((f: any) => (
<tr key={f.id} className="table-row">
<td className="table-cell font-medium">{f.razao_social}</td>
<td className="table-cell">
<span className={`badge ${f.classificacao_esg === 'Avançado' ? 'badge-success' : f.classificacao_esg === 'Intermediário' ? 'badge-warning' : 'badge-error'}`}>
{f.classificacao_esg || '-'}
</span>
</td>
<td className="table-cell">{Number(f.rating).toFixed(1)}</td>
<td className="table-cell">{f.possui_politica_ambiental ? '✅' : '❌'}</td>
<td className="table-cell">{f.possui_politica_sst ? '✅' : '❌'}</td>
<td className="table-cell">{f.declara_uso_epi ? '✅' : '❌'}</td>
<td className="table-cell">{f.equipe_treinada ? '✅' : '❌'}</td>
<td className="table-cell">{f.total_os}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)
}
const renderGenericTable = (items: any[]) => {
if (!items || items.length === 0) return <p className="text-gray text-center py-8">Nenhum dado.</p>
const keys = Object.keys(items[0]).filter(k => k !== 'id')
return (
<div className="overflow-x-auto">
<table className="w-full">
<thead><tr className="table-header">
{keys.map(k => <th key={k} className="table-cell text-left capitalize">{k.replace(/_/g, ' ')}</th>)}
</tr></thead>
<tbody>
{items.map((item: any, i: number) => (
<tr key={i} className="table-row">
{keys.map(k => (
<td key={k} className="table-cell text-sm">
{typeof item[k] === 'number' ? item[k].toLocaleString('pt-BR', { maximumFractionDigits: 2 }) : String(item[k] ?? '-')}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
const renderContent = () => {
if (loading) return <div className="flex justify-center py-12"><Loader2 className="w-8 h-8 animate-spin text-primary" /></div>
if (!data) return <div className="text-center py-12"><FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" /><p className="text-gray">Nenhum dado disponível.</p></div>
// ESG specific renderers
if (activeTab === 'esg_impacto') return renderEsgImpacto()
if (activeTab === 'esg_fornecedores') return renderEsgFornecedores()
if (activeTab === 'esg_governanca') {
const itens = data.itens || []
return (
<div>
<p className="text-sm text-gray mb-4">Total de exceções: <strong>{data.total}</strong></p>
{renderGenericTable(itens.map((w: any) => ({
demanda: w.demanda_numero ? `#${w.demanda_numero} - ${w.demanda_titulo}` : w.demanda_titulo,
valor: w.valor_total,
status: w.status,
})))}
</div>
)
}
// Generic
const items = Array.isArray(data) ? data : data?.items || data?.dados || data?.detalhes || []
if (Array.isArray(items) && items.length > 0) return renderGenericTable(items)
if (typeof data === 'object' && !Array.isArray(data)) {
const entries = Object.entries(data).filter(([k]) => !['items', 'dados', 'detalhes', 'itens', 'resumo', 'fornecedores'].includes(k))
if (entries.length > 0) {
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{entries.map(([k, v]) => (
<div key={k} className="p-4 bg-gray-50 rounded-xl">
<p className="text-sm text-gray capitalize">{k.replace(/_/g, ' ')}</p>
<p className="text-xl font-bold text-text">
{typeof v === 'number' ? v.toLocaleString('pt-BR', { maximumFractionDigits: 2 }) : String(v)}
</p>
</div>
))}
</div>
)
}
}
return <p className="text-center text-gray py-8">Formato de dados não reconhecido.</p>
}
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>
<h1 className="text-2xl sm:text-3xl font-bold text-text flex items-center gap-2">
<FileText className="w-8 h-8 text-primary" /> Relatórios
</h1>
<p className="text-gray mt-1">Relatórios detalhados por área.</p>
</div>
<div className="flex gap-3">
<select
value={period}
onChange={(e) => setPeriod(e.target.value)}
className="input-field w-40"
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray" />
<input type="date" value={dateFrom} onChange={e => setDateFrom(e.target.value)} className="input-field text-sm" />
<span className="text-gray"></span>
<input type="date" value={dateTo} onChange={e => setDateTo(e.target.value)} className="input-field text-sm" />
</div>
</div>
{/* Tabs */}
<div className="flex gap-1 bg-gray-100 p-1 rounded-xl overflow-x-auto">
{tabs.map(tab => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium transition-all whitespace-nowrap ${
activeTab === tab.key
? `bg-white shadow-sm ${tab.esg ? '' : 'text-primary'}`
: 'text-gray hover:text-text'
}`}
style={activeTab === tab.key && tab.esg ? { color: '#1A7A4C' } : {}}
>
<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
{tab.icon} {tab.label}
</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 */}
{/* Content */}
<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>
{renderContent()}
</div>
</div>
)