237 lines
10 KiB
TypeScript
237 lines
10 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { FileText, Loader2, Download, Calendar, TrendingUp, Building2, ClipboardList, Leaf } from 'lucide-react'
|
|
import api from '../services/api'
|
|
|
|
type TabKey = 'orcamento' | 'demandas' | 'fornecedores' | 'os' | 'esg_impacto' | 'esg_fornecedores' | 'esg_preventiva' | 'esg_governanca'
|
|
|
|
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 [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">
|
|
<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 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 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' } : {}}
|
|
>
|
|
{tab.icon} {tab.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="card">
|
|
{renderContent()}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|