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 <noreply@anthropic.com>
This commit is contained in:
parent
676978146b
commit
cbb4f265c5
@ -16,6 +16,7 @@ import { ManzanasPage } from './pages/admin/proyectos/ManzanasPage';
|
|||||||
import { DashboardPage } from './pages/admin/dashboard';
|
import { DashboardPage } from './pages/admin/dashboard';
|
||||||
import { ConceptosPage, PresupuestosPage, EstimacionesPage } from './pages/admin/presupuestos';
|
import { ConceptosPage, PresupuestosPage, EstimacionesPage } from './pages/admin/presupuestos';
|
||||||
import { OpportunitiesPage, TendersPage, ProposalsPage, VendorsPage } from './pages/admin/bidding';
|
import { OpportunitiesPage, TendersPage, ProposalsPage, VendorsPage } from './pages/admin/bidding';
|
||||||
|
import { IncidentesPage, CapacitacionesPage } from './pages/admin/hse';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -59,6 +60,13 @@ function App() {
|
|||||||
<Route path="propuestas" element={<ProposalsPage />} />
|
<Route path="propuestas" element={<ProposalsPage />} />
|
||||||
<Route path="proveedores" element={<VendorsPage />} />
|
<Route path="proveedores" element={<VendorsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
{/* HSE */}
|
||||||
|
<Route path="hse">
|
||||||
|
<Route index element={<Navigate to="incidentes" replace />} />
|
||||||
|
<Route path="incidentes" element={<IncidentesPage />} />
|
||||||
|
<Route path="capacitaciones" element={<CapacitacionesPage />} />
|
||||||
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Portal Supervisor */}
|
{/* Portal Supervisor */}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ import {
|
|||||||
FileCheck,
|
FileCheck,
|
||||||
Send,
|
Send,
|
||||||
Users,
|
Users,
|
||||||
|
AlertTriangle,
|
||||||
|
GraduationCap,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
@ -74,6 +76,14 @@ const navSections: NavSection[] = [
|
|||||||
{ label: 'Proveedores', href: '/admin/licitaciones/proveedores', icon: Users },
|
{ 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() {
|
export function AdminLayout() {
|
||||||
|
|||||||
446
web/src/pages/admin/hse/CapacitacionesPage.tsx
Normal file
446
web/src/pages/admin/hse/CapacitacionesPage.tsx
Normal file
@ -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<TipoCapacitacion, string> = {
|
||||||
|
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<TipoCapacitacion, string> = {
|
||||||
|
induccion: 'Induccion',
|
||||||
|
especifica: 'Especifica',
|
||||||
|
certificacion: 'Certificacion',
|
||||||
|
reentrenamiento: 'Reentrenamiento',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function CapacitacionesPage() {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [tipoFilter, setTipoFilter] = useState<TipoCapacitacion | ''>('');
|
||||||
|
const [activoFilter, setActivoFilter] = useState<boolean | ''>('');
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [editingItem, setEditingItem] = useState<Capacitacion | null>(null);
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(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 (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Capacitaciones HSE</h1>
|
||||||
|
<p className="text-gray-600">Gestion de capacitaciones de seguridad y salud</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingItem(null);
|
||||||
|
setShowModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="w-5 h-5 mr-2" />
|
||||||
|
Nueva Capacitacion
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Buscar por codigo o nombre..."
|
||||||
|
className="w-full pl-10 pr-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
className="px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
value={tipoFilter}
|
||||||
|
onChange={(e) => setTipoFilter(e.target.value as TipoCapacitacion | '')}
|
||||||
|
>
|
||||||
|
<option value="">Todos los tipos</option>
|
||||||
|
{Object.entries(tipoLabels).map(([value, label]) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className="px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
value={activoFilter === '' ? '' : activoFilter ? 'true' : 'false'}
|
||||||
|
onChange={(e) =>
|
||||||
|
setActivoFilter(e.target.value === '' ? '' : e.target.value === 'true')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">Todos los estados</option>
|
||||||
|
<option value="true">Activos</option>
|
||||||
|
<option value="false">Inactivos</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="p-8 text-center text-gray-500">Cargando...</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="p-8 text-center text-red-500">Error al cargar los datos</div>
|
||||||
|
) : capacitaciones.length === 0 ? (
|
||||||
|
<div className="p-8 text-center text-gray-500">No hay capacitaciones</div>
|
||||||
|
) : (
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
|
Codigo
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
|
Nombre
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
|
Tipo
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
|
Duracion
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||||
|
Estado
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">
|
||||||
|
Acciones
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{capacitaciones.map((item) => (
|
||||||
|
<tr key={item.id} className="hover:bg-gray-50">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
|
{item.codigo}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-900">{item.nombre}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'px-2 py-1 text-xs font-medium rounded-full',
|
||||||
|
tipoColors[item.tipo]
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tipoLabels[item.tipo]}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{item.duracionHoras} hrs
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'px-2 py-1 text-xs font-medium rounded-full',
|
||||||
|
item.activo
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.activo ? 'Activo' : 'Inactivo'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
'p-2 rounded-lg',
|
||||||
|
item.activo
|
||||||
|
? 'text-gray-500 hover:text-orange-600 hover:bg-orange-50'
|
||||||
|
: 'text-gray-500 hover:text-green-600 hover:bg-green-50'
|
||||||
|
)}
|
||||||
|
title={item.activo ? 'Desactivar' : 'Activar'}
|
||||||
|
onClick={() => handleToggleActive(item.id)}
|
||||||
|
disabled={toggleMutation.isPending}
|
||||||
|
>
|
||||||
|
<Power className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg"
|
||||||
|
title="Editar"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingItem(item);
|
||||||
|
setShowModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="p-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg"
|
||||||
|
title="Eliminar"
|
||||||
|
onClick={() => setDeleteConfirm(item.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
{showModal && (
|
||||||
|
<CapacitacionModal
|
||||||
|
item={editingItem}
|
||||||
|
onClose={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setEditingItem(null);
|
||||||
|
}}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
isLoading={createMutation.isPending || updateMutation.isPending}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Delete Confirmation */}
|
||||||
|
{deleteConfirm && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Confirmar eliminacion</h3>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
¿Esta seguro de eliminar esta capacitacion? Esta accion no se puede deshacer.
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 text-gray-700 border rounded-lg hover:bg-gray-50"
|
||||||
|
onClick={() => setDeleteConfirm(null)}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
|
||||||
|
onClick={() => handleDelete(deleteConfirm)}
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
|
>
|
||||||
|
{deleteMutation.isPending ? 'Eliminando...' : 'Eliminar'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Component
|
||||||
|
interface CapacitacionModalProps {
|
||||||
|
item: Capacitacion | null;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (data: CreateCapacitacionDto) => Promise<void>;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CapacitacionModal({ item, onClose, onSubmit, isLoading }: CapacitacionModalProps) {
|
||||||
|
const [formData, setFormData] = useState<CreateCapacitacionDto>({
|
||||||
|
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 (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">
|
||||||
|
{item ? 'Editar Capacitacion' : 'Nueva Capacitacion'}
|
||||||
|
</h3>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Codigo *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
value={formData.codigo}
|
||||||
|
onChange={(e) => setFormData({ ...formData, codigo: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Tipo *</label>
|
||||||
|
<select
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
value={formData.tipo}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, tipo: e.target.value as TipoCapacitacion })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Object.entries(tipoLabels).map(([value, label]) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Nombre *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
value={formData.nombre}
|
||||||
|
onChange={(e) => setFormData({ ...formData, nombre: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Descripcion</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
value={formData.descripcion}
|
||||||
|
onChange={(e) => setFormData({ ...formData, descripcion: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Duracion (horas) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
min="0"
|
||||||
|
step="0.5"
|
||||||
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
value={formData.duracionHoras}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, duracionHoras: parseFloat(e.target.value) })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<label className="flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||||
|
checked={formData.activo}
|
||||||
|
onChange={(e) => setFormData({ ...formData, activo: e.target.checked })}
|
||||||
|
/>
|
||||||
|
<span className="ml-2 text-sm text-gray-700">Activo</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Objetivos</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
value={formData.objetivos}
|
||||||
|
onChange={(e) => setFormData({ ...formData, objetivos: e.target.value })}
|
||||||
|
placeholder="Objetivos de aprendizaje de la capacitacion"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Temario</label>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
value={formData.temario}
|
||||||
|
onChange={(e) => setFormData({ ...formData, temario: e.target.value })}
|
||||||
|
placeholder="Contenido y temas a cubrir"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Requisitos previos
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={2}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||||
|
value={formData.requisitos}
|
||||||
|
onChange={(e) => setFormData({ ...formData, requisitos: e.target.value })}
|
||||||
|
placeholder="Requisitos para tomar la capacitacion"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 text-gray-700 border rounded-lg hover:bg-gray-50"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Guardando...' : item ? 'Guardar Cambios' : 'Crear'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
web/src/pages/admin/hse/IncidentesPage.tsx
Normal file
46
web/src/pages/admin/hse/IncidentesPage.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* IncidentesPage Component
|
||||||
|
* Gestion de incidentes HSE
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AlertTriangle } from 'lucide-react';
|
||||||
|
|
||||||
|
export function IncidentesPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Incidentes HSE</h1>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Gestion de incidentes de seguridad, salud y medio ambiente
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-lg shadow-sm p-8">
|
||||||
|
<div className="flex flex-col items-center justify-center text-center py-12">
|
||||||
|
<div className="w-16 h-16 bg-yellow-100 rounded-full flex items-center justify-center mb-4">
|
||||||
|
<AlertTriangle className="w-8 h-8 text-yellow-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||||
|
Modulo de Incidentes en Desarrollo
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 max-w-md">
|
||||||
|
Este modulo permitira registrar, clasificar y dar seguimiento a incidentes
|
||||||
|
de seguridad, salud ocupacional y medio ambiente.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 text-sm text-gray-500">
|
||||||
|
<p>Funcionalidades proximas:</p>
|
||||||
|
<ul className="mt-2 space-y-1">
|
||||||
|
<li>- Registro de incidentes y accidentes</li>
|
||||||
|
<li>- Clasificacion por severidad y tipo</li>
|
||||||
|
<li>- Seguimiento de investigaciones</li>
|
||||||
|
<li>- Planes de accion correctiva</li>
|
||||||
|
<li>- Reportes y estadisticas</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
web/src/pages/admin/hse/index.ts
Normal file
2
web/src/pages/admin/hse/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { IncidentesPage } from './IncidentesPage';
|
||||||
|
export { CapacitacionesPage } from './CapacitacionesPage';
|
||||||
Loading…
Reference in New Issue
Block a user