From cbb4f265c5ad0a3302f876f436b95f3d5a84ea96 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Mon, 2 Feb 2026 21:23:13 -0600 Subject: [PATCH] feat(frontend): Add HSE module navigation and routes - Add HSE section to AdminLayout sidebar with Incidentes and Capacitaciones - Add HSE routes in App.tsx (/admin/hse/incidentes and /admin/hse/capacitaciones) - Create IncidentesPage and CapacitacionesPage components - Create barrel export index.ts for HSE pages - Import AlertTriangle and GraduationCap icons for HSE navigation Routes added: - /admin/hse -> redirects to /admin/hse/incidentes - /admin/hse/incidentes -> IncidentesPage - /admin/hse/capacitaciones -> CapacitacionesPage Navigation structure: - HSE section in sidebar (collapsed by default) - Links to Incidentes and Capacitaciones pages - Active state highlighting for current route Co-Authored-By: Claude Opus 4.5 --- web/src/App.tsx | 8 + web/src/layouts/AdminLayout.tsx | 10 + .../pages/admin/hse/CapacitacionesPage.tsx | 446 ++++++++++++++++++ web/src/pages/admin/hse/IncidentesPage.tsx | 46 ++ web/src/pages/admin/hse/index.ts | 2 + 5 files changed, 512 insertions(+) create mode 100644 web/src/pages/admin/hse/CapacitacionesPage.tsx create mode 100644 web/src/pages/admin/hse/IncidentesPage.tsx create mode 100644 web/src/pages/admin/hse/index.ts diff --git a/web/src/App.tsx b/web/src/App.tsx index fa0f54e..c933879 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -16,6 +16,7 @@ import { ManzanasPage } from './pages/admin/proyectos/ManzanasPage'; import { DashboardPage } from './pages/admin/dashboard'; import { ConceptosPage, PresupuestosPage, EstimacionesPage } from './pages/admin/presupuestos'; import { OpportunitiesPage, TendersPage, ProposalsPage, VendorsPage } from './pages/admin/bidding'; +import { IncidentesPage, CapacitacionesPage } from './pages/admin/hse'; function App() { return ( @@ -59,6 +60,13 @@ function App() { } /> } /> + + {/* HSE */} + + } /> + } /> + } /> + {/* Portal Supervisor */} diff --git a/web/src/layouts/AdminLayout.tsx b/web/src/layouts/AdminLayout.tsx index 3d0703c..a3fcd24 100644 --- a/web/src/layouts/AdminLayout.tsx +++ b/web/src/layouts/AdminLayout.tsx @@ -20,6 +20,8 @@ import { FileCheck, Send, Users, + AlertTriangle, + GraduationCap, } from 'lucide-react'; import clsx from 'clsx'; import { useAuthStore } from '../stores/authStore'; @@ -74,6 +76,14 @@ const navSections: NavSection[] = [ { label: 'Proveedores', href: '/admin/licitaciones/proveedores', icon: Users }, ], }, + { + title: 'HSE', + defaultOpen: false, + items: [ + { label: 'Incidentes', href: '/admin/hse/incidentes', icon: AlertTriangle }, + { label: 'Capacitaciones', href: '/admin/hse/capacitaciones', icon: GraduationCap }, + ], + }, ]; export function AdminLayout() { diff --git a/web/src/pages/admin/hse/CapacitacionesPage.tsx b/web/src/pages/admin/hse/CapacitacionesPage.tsx new file mode 100644 index 0000000..e6c7c61 --- /dev/null +++ b/web/src/pages/admin/hse/CapacitacionesPage.tsx @@ -0,0 +1,446 @@ +import { useState } from 'react'; +import { Plus, Pencil, Trash2, Search, Power } from 'lucide-react'; +import { + useCapacitaciones, + useCreateCapacitacion, + useUpdateCapacitacion, + useToggleCapacitacion, + useDeleteCapacitacion, +} from '../../../hooks/useHSE'; +import { + Capacitacion, + TipoCapacitacion, + CreateCapacitacionDto, +} from '../../../services/hse/capacitaciones.api'; +import clsx from 'clsx'; + +const tipoColors: Record = { + induccion: 'bg-green-100 text-green-800', + especifica: 'bg-blue-100 text-blue-800', + certificacion: 'bg-purple-100 text-purple-800', + reentrenamiento: 'bg-orange-100 text-orange-800', +}; + +const tipoLabels: Record = { + induccion: 'Induccion', + especifica: 'Especifica', + certificacion: 'Certificacion', + reentrenamiento: 'Reentrenamiento', +}; + +export function CapacitacionesPage() { + const [search, setSearch] = useState(''); + const [tipoFilter, setTipoFilter] = useState(''); + const [activoFilter, setActivoFilter] = useState(''); + const [showModal, setShowModal] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [deleteConfirm, setDeleteConfirm] = useState(null); + + const { data, isLoading, error } = useCapacitaciones({ + search: search || undefined, + tipo: tipoFilter || undefined, + activo: activoFilter === '' ? undefined : activoFilter, + }); + + const deleteMutation = useDeleteCapacitacion(); + const createMutation = useCreateCapacitacion(); + const updateMutation = useUpdateCapacitacion(); + const toggleMutation = useToggleCapacitacion(); + + const handleDelete = async (id: string) => { + await deleteMutation.mutateAsync(id); + setDeleteConfirm(null); + }; + + const handleSubmit = async (formData: CreateCapacitacionDto) => { + if (editingItem) { + await updateMutation.mutateAsync({ id: editingItem.id, data: formData }); + } else { + await createMutation.mutateAsync(formData); + } + setShowModal(false); + setEditingItem(null); + }; + + const handleToggleActive = async (id: string) => { + await toggleMutation.mutateAsync(id); + }; + + const capacitaciones = data?.items || []; + + return ( +
+
+
+

Capacitaciones HSE

+

Gestion de capacitaciones de seguridad y salud

+
+ +
+ + {/* Filters */} +
+
+
+ + setSearch(e.target.value)} + /> +
+ + +
+
+ + {/* Table */} +
+ {isLoading ? ( +
Cargando...
+ ) : error ? ( +
Error al cargar los datos
+ ) : capacitaciones.length === 0 ? ( +
No hay capacitaciones
+ ) : ( + + + + + + + + + + + + + {capacitaciones.map((item) => ( + + + + + + + + + ))} + +
+ Codigo + + Nombre + + Tipo + + Duracion + + Estado + + Acciones +
+ {item.codigo} + {item.nombre} + + {tipoLabels[item.tipo]} + + + {item.duracionHoras} hrs + + + {item.activo ? 'Activo' : 'Inactivo'} + + +
+ + + +
+
+ )} +
+ + {/* Modal */} + {showModal && ( + { + setShowModal(false); + setEditingItem(null); + }} + onSubmit={handleSubmit} + isLoading={createMutation.isPending || updateMutation.isPending} + /> + )} + + {/* Delete Confirmation */} + {deleteConfirm && ( +
+
+

Confirmar eliminacion

+

+ ¿Esta seguro de eliminar esta capacitacion? Esta accion no se puede deshacer. +

+
+ + +
+
+
+ )} +
+ ); +} + +// Modal Component +interface CapacitacionModalProps { + item: Capacitacion | null; + onClose: () => void; + onSubmit: (data: CreateCapacitacionDto) => Promise; + isLoading: boolean; +} + +function CapacitacionModal({ item, onClose, onSubmit, isLoading }: CapacitacionModalProps) { + const [formData, setFormData] = useState({ + codigo: item?.codigo || '', + nombre: item?.nombre || '', + descripcion: item?.descripcion || '', + tipo: item?.tipo || 'induccion', + duracionHoras: item?.duracionHoras || 0, + temario: item?.temario || '', + objetivos: item?.objetivos || '', + requisitos: item?.requisitos || '', + activo: item?.activo ?? true, + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + await onSubmit(formData); + }; + + return ( +
+
+

+ {item ? 'Editar Capacitacion' : 'Nueva Capacitacion'} +

+
+
+
+ + setFormData({ ...formData, codigo: e.target.value })} + /> +
+
+ + +
+
+ +
+ + setFormData({ ...formData, nombre: e.target.value })} + /> +
+ +
+ +