Major update: ESG, KPIs, metas, alertas, auditoria, documentos, importação, relatórios, subcategorias, dashboard orçamentos
This commit is contained in:
432
frontend/src/pages/Configuracao.tsx
Normal file
432
frontend/src/pages/Configuracao.tsx
Normal file
@@ -0,0 +1,432 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Settings, Plus, Edit2, Trash2, Loader2, X, Save } from 'lucide-react'
|
||||
import api from '../services/api'
|
||||
import Modal from '../components/Modal'
|
||||
|
||||
type Tab = 'categorias' | 'centros_custo' | 'subcategorias' | 'locais'
|
||||
|
||||
const tabs: { key: Tab; label: string }[] = [
|
||||
{ key: 'categorias', label: 'Categorias' },
|
||||
{ key: 'centros_custo', label: 'Centros de Custo' },
|
||||
{ key: 'subcategorias', label: 'Subcategorias' },
|
||||
{ key: 'locais', label: 'Locais' },
|
||||
]
|
||||
|
||||
export default function Configuracao() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('categorias')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [items, setItems] = useState<any[]>([])
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [editId, setEditId] = useState<string | null>(null)
|
||||
const [form, setForm] = useState<any>({})
|
||||
const [saving, setSaving] = useState(false)
|
||||
// For subcategorias
|
||||
const [categorias, setCategorias] = useState<any[]>([])
|
||||
|
||||
const endpoints: Record<Tab, string> = {
|
||||
categorias: '/categorias',
|
||||
centros_custo: '/centros-custo',
|
||||
subcategorias: '/subcategorias',
|
||||
locais: '/locais',
|
||||
}
|
||||
|
||||
useEffect(() => { fetchItems(); fetchCategorias() }, [activeTab])
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const { data } = await api.get(endpoints[activeTab])
|
||||
setItems(data)
|
||||
} catch { setItems([]) }
|
||||
finally { setLoading(false) }
|
||||
}
|
||||
|
||||
const fetchCategorias = async () => {
|
||||
try {
|
||||
const { data } = await api.get('/categorias')
|
||||
setCategorias(data)
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const getEmptyForm = (): any => {
|
||||
switch (activeTab) {
|
||||
case 'categorias': return { nome: '', criticidade_padrao: 'media', sla_dias: 30, tipo_investimento: '', tipo_manutencao: '', impacto_ambiental_esperado: '', potencial_geracao_residuos: '' }
|
||||
case 'centros_custo': return { codigo: '', nome: '' }
|
||||
case 'subcategorias': return { nome: '', categoria_id: '' }
|
||||
case 'locais': return { nome: '', endereco: '', tipo_operacao_local: '', classificacao_impacto_ambiental: '', praticas_sustentaveis: [] }
|
||||
}
|
||||
}
|
||||
|
||||
const openNew = () => { setEditId(null); setForm(getEmptyForm()); setShowModal(true) }
|
||||
|
||||
const openEdit = (item: any) => {
|
||||
setEditId(item.id)
|
||||
switch (activeTab) {
|
||||
case 'categorias':
|
||||
setForm({ nome: item.nome, criticidade_padrao: item.criticidade_padrao || 'media', sla_dias: item.sla_dias || 30, tipo_investimento: item.tipo_investimento || '', tipo_manutencao: item.tipo_manutencao || '', impacto_ambiental_esperado: item.impacto_ambiental_esperado || '', potencial_geracao_residuos: item.potencial_geracao_residuos || '' })
|
||||
break
|
||||
case 'centros_custo':
|
||||
setForm({ codigo: item.codigo || '', nome: item.nome })
|
||||
break
|
||||
case 'subcategorias':
|
||||
setForm({ nome: item.nome, categoria_id: item.categoria_id || '' })
|
||||
break
|
||||
case 'locais':
|
||||
setForm({ nome: item.nome, endereco: item.endereco || '', tipo_operacao_local: item.tipo_operacao_local || '', classificacao_impacto_ambiental: item.classificacao_impacto_ambiental || '', praticas_sustentaveis: item.praticas_sustentaveis || [] })
|
||||
break
|
||||
}
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
if (editId) {
|
||||
await api.patch(`${endpoints[activeTab]}/${editId}`, form)
|
||||
} else {
|
||||
await api.post(endpoints[activeTab], form)
|
||||
}
|
||||
setShowModal(false)
|
||||
fetchItems()
|
||||
} catch (err) {
|
||||
alert('Erro ao salvar')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
if (!confirm('Tem certeza que deseja excluir?')) return
|
||||
try {
|
||||
await api.delete(`${endpoints[activeTab]}/${id}`)
|
||||
fetchItems()
|
||||
} catch { alert('Erro ao excluir') }
|
||||
}
|
||||
|
||||
const catMap: Record<string, string> = {}
|
||||
categorias.forEach(c => { catMap[c.id] = c.nome })
|
||||
|
||||
const renderFormFields = () => {
|
||||
switch (activeTab) {
|
||||
case 'categorias':
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Nome</label>
|
||||
<input type="text" value={form.nome} onChange={e => setForm({ ...form, nome: e.target.value })} className="input-field" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Tipo de Investimento *</label>
|
||||
<select value={form.tipo_investimento} onChange={e => setForm({ ...form, tipo_investimento: e.target.value })} className="input-field" required>
|
||||
<option value="">Selecione...</option>
|
||||
<option value="capex">Capex</option>
|
||||
<option value="opex">Opex</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Criticidade Padrão</label>
|
||||
<select value={form.criticidade_padrao} onChange={e => setForm({ ...form, criticidade_padrao: e.target.value })} className="input-field">
|
||||
<option value="baixa">Baixa</option>
|
||||
<option value="media">Média</option>
|
||||
<option value="alta">Alta</option>
|
||||
<option value="critica">Crítica</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">SLA (dias)</label>
|
||||
<input type="number" value={form.sla_dias} onChange={e => setForm({ ...form, sla_dias: Number(e.target.value) })} className="input-field" />
|
||||
</div>
|
||||
<div className="col-span-full border-t border-border pt-3 mt-2">
|
||||
<p className="text-sm font-semibold mb-3" style={{ color: '#1A7A4C' }}>🌿 Campos ESG</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Tipo de Manutenção</label>
|
||||
<select value={form.tipo_manutencao} onChange={e => setForm({ ...form, tipo_manutencao: e.target.value })} className="input-field">
|
||||
<option value="">Selecione...</option>
|
||||
<option value="Preventiva">Preventiva</option>
|
||||
<option value="Corretiva">Corretiva</option>
|
||||
<option value="Emergencial">Emergencial</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Impacto Ambiental Esperado</label>
|
||||
<select value={form.impacto_ambiental_esperado} onChange={e => setForm({ ...form, impacto_ambiental_esperado: e.target.value })} className="input-field">
|
||||
<option value="">Selecione...</option>
|
||||
<option value="Baixo">Baixo</option>
|
||||
<option value="Médio">Médio</option>
|
||||
<option value="Alto">Alto</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Potencial Geração Resíduos</label>
|
||||
<select value={form.potencial_geracao_residuos} onChange={e => setForm({ ...form, potencial_geracao_residuos: e.target.value })} className="input-field">
|
||||
<option value="">Selecione...</option>
|
||||
<option value="Baixo">Baixo</option>
|
||||
<option value="Médio">Médio</option>
|
||||
<option value="Alto">Alto</option>
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case 'centros_custo':
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Código</label>
|
||||
<input type="text" value={form.codigo} onChange={e => setForm({ ...form, codigo: e.target.value })} className="input-field" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Nome</label>
|
||||
<input type="text" value={form.nome} onChange={e => setForm({ ...form, nome: e.target.value })} className="input-field" required />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case 'subcategorias':
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Nome</label>
|
||||
<input type="text" value={form.nome} onChange={e => setForm({ ...form, nome: e.target.value })} className="input-field" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Categoria</label>
|
||||
<select value={form.categoria_id} onChange={e => setForm({ ...form, categoria_id: e.target.value })} className="input-field" required>
|
||||
<option value="">Selecione...</option>
|
||||
{categorias.map(c => <option key={c.id} value={c.id}>{c.nome}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
case 'locais':
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Nome</label>
|
||||
<input type="text" value={form.nome} onChange={e => setForm({ ...form, nome: e.target.value })} className="input-field" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Endereço</label>
|
||||
<input type="text" value={form.endereco} onChange={e => setForm({ ...form, endereco: e.target.value })} className="input-field" />
|
||||
</div>
|
||||
<div className="col-span-full border-t border-border pt-3 mt-2">
|
||||
<p className="text-sm font-semibold mb-3" style={{ color: '#1A7A4C' }}>🌿 Campos ESG</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Tipo de Operação</label>
|
||||
<select value={form.tipo_operacao_local} onChange={e => setForm({ ...form, tipo_operacao_local: e.target.value })} className="input-field">
|
||||
<option value="">Selecione...</option>
|
||||
<option value="Administrativo">Administrativo</option>
|
||||
<option value="Industrial">Industrial</option>
|
||||
<option value="Logístico">Logístico</option>
|
||||
<option value="Comercial">Comercial</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Classificação Impacto Ambiental</label>
|
||||
<select value={form.classificacao_impacto_ambiental} onChange={e => setForm({ ...form, classificacao_impacto_ambiental: e.target.value })} className="input-field">
|
||||
<option value="">Selecione...</option>
|
||||
<option value="Baixo">Baixo</option>
|
||||
<option value="Médio">Médio</option>
|
||||
<option value="Alto">Alto</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text mb-2">Práticas Sustentáveis</label>
|
||||
<div className="space-y-2">
|
||||
{['Coleta Seletiva', 'Reuso de Água', 'Energia Renovável', 'Compostagem', 'Redução de Plástico'].map(p => (
|
||||
<label key={p} className="flex items-center gap-2 text-sm">
|
||||
<input type="checkbox" checked={(form.praticas_sustentaveis || []).includes(p)}
|
||||
onChange={e => {
|
||||
const curr = form.praticas_sustentaveis || []
|
||||
setForm({ ...form, praticas_sustentaveis: e.target.checked ? [...curr, p] : curr.filter((x: string) => x !== p) })
|
||||
}}
|
||||
className="rounded border-gray-300 text-green-600" />
|
||||
{p}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const renderColumns = () => {
|
||||
switch (activeTab) {
|
||||
case 'categorias':
|
||||
return (
|
||||
<>
|
||||
<th className="table-cell">Nome</th>
|
||||
<th className="table-cell">Tipo Invest.</th>
|
||||
<th className="table-cell">Criticidade</th>
|
||||
<th className="table-cell">SLA</th>
|
||||
<th className="table-cell">Manutenção</th>
|
||||
<th className="table-cell">Impacto Amb.</th>
|
||||
<th className="table-cell text-center">Ações</th>
|
||||
</>
|
||||
)
|
||||
case 'centros_custo':
|
||||
return (
|
||||
<>
|
||||
<th className="table-cell">Código</th>
|
||||
<th className="table-cell">Nome</th>
|
||||
<th className="table-cell text-center">Ações</th>
|
||||
</>
|
||||
)
|
||||
case 'subcategorias':
|
||||
return (
|
||||
<>
|
||||
<th className="table-cell">Nome</th>
|
||||
<th className="table-cell">Categoria</th>
|
||||
<th className="table-cell text-center">Ações</th>
|
||||
</>
|
||||
)
|
||||
case 'locais':
|
||||
return (
|
||||
<>
|
||||
<th className="table-cell">Nome</th>
|
||||
<th className="table-cell">Endereço</th>
|
||||
<th className="table-cell">Operação</th>
|
||||
<th className="table-cell">Impacto Amb.</th>
|
||||
<th className="table-cell text-center">Ações</th>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const renderRow = (item: any) => {
|
||||
switch (activeTab) {
|
||||
case 'categorias':
|
||||
return (
|
||||
<>
|
||||
<td className="table-cell font-medium">{item.nome}</td>
|
||||
<td className="table-cell">
|
||||
{item.tipo_investimento ? (
|
||||
<span className={`badge ${item.tipo_investimento === 'capex' ? 'badge-info' : 'badge-warning'}`}>
|
||||
{item.tipo_investimento === 'capex' ? 'Capex' : 'Opex'}
|
||||
</span>
|
||||
) : '-'}
|
||||
</td>
|
||||
<td className="table-cell">{item.criticidade_padrao || '-'}</td>
|
||||
<td className="table-cell">{item.sla_dias} dias</td>
|
||||
<td className="table-cell">
|
||||
{item.tipo_manutencao ? (
|
||||
<span className={`badge ${item.tipo_manutencao === 'Emergencial' ? 'badge-error' : item.tipo_manutencao === 'Corretiva' ? 'badge-warning' : 'badge-success'}`}>
|
||||
{item.tipo_manutencao}
|
||||
</span>
|
||||
) : '-'}
|
||||
</td>
|
||||
<td className="table-cell">
|
||||
{item.impacto_ambiental_esperado ? (
|
||||
<span className={`badge ${item.impacto_ambiental_esperado === 'Alto' ? 'badge-error' : item.impacto_ambiental_esperado === 'Médio' ? 'badge-warning' : 'badge-success'}`}>
|
||||
{item.impacto_ambiental_esperado}
|
||||
</span>
|
||||
) : '-'}
|
||||
</td>
|
||||
</>
|
||||
)
|
||||
case 'centros_custo':
|
||||
return (
|
||||
<>
|
||||
<td className="table-cell font-mono">{item.codigo}</td>
|
||||
<td className="table-cell font-medium">{item.nome}</td>
|
||||
</>
|
||||
)
|
||||
case 'subcategorias':
|
||||
return (
|
||||
<>
|
||||
<td className="table-cell font-medium">{item.nome}</td>
|
||||
<td className="table-cell">{catMap[item.categoria_id] || '-'}</td>
|
||||
</>
|
||||
)
|
||||
case 'locais':
|
||||
return (
|
||||
<>
|
||||
<td className="table-cell font-medium">{item.nome}</td>
|
||||
<td className="table-cell">{item.endereco || '-'}</td>
|
||||
<td className="table-cell">{item.tipo_operacao_local || '-'}</td>
|
||||
<td className="table-cell">
|
||||
{item.classificacao_impacto_ambiental ? (
|
||||
<span className={`badge ${item.classificacao_impacto_ambiental === 'Alto' ? 'badge-error' : item.classificacao_impacto_ambiental === 'Médio' ? 'badge-warning' : 'badge-success'}`}>
|
||||
{item.classificacao_impacto_ambiental}
|
||||
</span>
|
||||
) : '-'}
|
||||
</td>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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-3">
|
||||
<Settings className="w-8 h-8 text-primary" /> Configuração
|
||||
</h1>
|
||||
<p className="text-gray mt-1">Gerencie as tabelas de apoio do sistema</p>
|
||||
</div>
|
||||
<button onClick={openNew} className="btn-primary flex items-center gap-2 w-full sm:w-auto justify-center">
|
||||
<Plus className="w-5 h-5" />Novo Registro
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 border-b border-border overflow-x-auto">
|
||||
{tabs.map(tab => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={`px-4 py-3 text-sm font-medium transition-all border-b-2 whitespace-nowrap ${
|
||||
activeTab === tab.key
|
||||
? 'border-primary text-primary'
|
||||
: 'border-transparent text-gray hover:text-text'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-48"><Loader2 className="w-8 h-8 animate-spin text-primary" /></div>
|
||||
) : (
|
||||
<div className="card !p-0 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="table-header">
|
||||
<tr>{renderColumns()}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map(item => (
|
||||
<tr key={item.id} className="table-row">
|
||||
{renderRow(item)}
|
||||
<td className="table-cell">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button onClick={() => openEdit(item)} className="p-2 rounded-lg hover:bg-gray-100 text-gray hover:text-primary transition-colors">
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(item.id)} className="p-2 rounded-lg hover:bg-red-50 text-gray hover:text-red-500 transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{items.length === 0 && (
|
||||
<div className="text-center py-12"><p className="text-gray">Nenhum registro encontrado</p></div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<Modal open={showModal} onClose={() => setShowModal(false)} title={editId ? 'Editar' : 'Novo Registro'} onSubmit={handleSubmit} submitLabel={editId ? 'Salvar' : 'Criar'} loading={saving}>
|
||||
{renderFormFields()}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user