feat(hse): Add IncidentesPage component for HSE incidents management
- Complete CRUD for HSE incidents (accidente, incidente, casi_accidente) - Filters by tipo, gravedad, estado, fraccionamiento, date range - Investigation workflow (abierto -> en_investigacion -> cerrado) - Modal forms for create/edit, investigate, and close - Color-coded badges for tipo, gravedad, and estado Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
dac9ae6f19
commit
744545defb
@ -1,45 +1,739 @@
|
||||
/**
|
||||
* IncidentesPage Component
|
||||
* Gestion de incidentes HSE
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Plus,
|
||||
Pencil,
|
||||
Eye,
|
||||
Search,
|
||||
FileText,
|
||||
XCircle,
|
||||
AlertTriangle,
|
||||
Activity,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
useIncidentes,
|
||||
useCreateIncidente,
|
||||
useUpdateIncidente,
|
||||
useInvestigateIncidente,
|
||||
useCloseIncidente,
|
||||
} from '../../../hooks/useHSE';
|
||||
import { useFraccionamientos } from '../../../hooks/useConstruccion';
|
||||
import { Fraccionamiento } from '../../../services/construccion/fraccionamientos.api';
|
||||
import {
|
||||
Incidente,
|
||||
TipoIncidente,
|
||||
GravedadIncidente,
|
||||
EstadoIncidente,
|
||||
CreateIncidenteDto,
|
||||
} from '../../../services/hse/incidentes.api';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
const tipoColors: Record<TipoIncidente, string> = {
|
||||
accidente: 'bg-red-100 text-red-800',
|
||||
incidente: 'bg-yellow-100 text-yellow-800',
|
||||
casi_accidente: 'bg-blue-100 text-blue-800',
|
||||
};
|
||||
|
||||
const tipoLabels: Record<TipoIncidente, string> = {
|
||||
accidente: 'Accidente',
|
||||
incidente: 'Incidente',
|
||||
casi_accidente: 'Casi Accidente',
|
||||
};
|
||||
|
||||
const gravedadColors: Record<GravedadIncidente, string> = {
|
||||
leve: 'bg-green-100 text-green-800',
|
||||
moderado: 'bg-yellow-100 text-yellow-800',
|
||||
grave: 'bg-orange-100 text-orange-800',
|
||||
fatal: 'bg-red-100 text-red-800',
|
||||
};
|
||||
|
||||
const gravedadLabels: Record<GravedadIncidente, string> = {
|
||||
leve: 'Leve',
|
||||
moderado: 'Moderado',
|
||||
grave: 'Grave',
|
||||
fatal: 'Fatal',
|
||||
};
|
||||
|
||||
const estadoColors: Record<EstadoIncidente, string> = {
|
||||
abierto: 'bg-yellow-100 text-yellow-800',
|
||||
en_investigacion: 'bg-blue-100 text-blue-800',
|
||||
cerrado: 'bg-green-100 text-green-800',
|
||||
};
|
||||
|
||||
const estadoLabels: Record<EstadoIncidente, string> = {
|
||||
abierto: 'Abierto',
|
||||
en_investigacion: 'En Investigación',
|
||||
cerrado: 'Cerrado',
|
||||
};
|
||||
|
||||
export function IncidentesPage() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [tipoFilter, setTipoFilter] = useState<TipoIncidente | ''>('');
|
||||
const [gravedadFilter, setGravedadFilter] = useState<GravedadIncidente | ''>('');
|
||||
const [estadoFilter, setEstadoFilter] = useState<EstadoIncidente | ''>('');
|
||||
const [fraccionamientoFilter, setFraccionamientoFilter] = useState('');
|
||||
const [dateFrom, setDateFrom] = useState('');
|
||||
const [dateTo, setDateTo] = useState('');
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<Incidente | null>(null);
|
||||
const [investigateModal, setInvestigateModal] = useState<Incidente | null>(null);
|
||||
const [closeModal, setCloseModal] = useState<Incidente | null>(null);
|
||||
|
||||
const { data, isLoading, error } = useIncidentes({
|
||||
search: search || undefined,
|
||||
tipo: tipoFilter || undefined,
|
||||
gravedad: gravedadFilter || undefined,
|
||||
estado: estadoFilter || undefined,
|
||||
fraccionamientoId: fraccionamientoFilter || undefined,
|
||||
dateFrom: dateFrom || undefined,
|
||||
dateTo: dateTo || undefined,
|
||||
});
|
||||
|
||||
const { data: fraccionamientosData } = useFraccionamientos();
|
||||
|
||||
const createMutation = useCreateIncidente();
|
||||
const updateMutation = useUpdateIncidente();
|
||||
const investigateMutation = useInvestigateIncidente();
|
||||
const closeMutation = useCloseIncidente();
|
||||
|
||||
const handleSubmit = async (formData: CreateIncidenteDto) => {
|
||||
if (editingItem) {
|
||||
await updateMutation.mutateAsync({ id: editingItem.id, data: formData });
|
||||
} else {
|
||||
await createMutation.mutateAsync(formData);
|
||||
}
|
||||
setShowModal(false);
|
||||
setEditingItem(null);
|
||||
};
|
||||
|
||||
const handleInvestigate = async (id: string, investigadorId: string) => {
|
||||
await investigateMutation.mutateAsync({
|
||||
id,
|
||||
investigadorId,
|
||||
fechaInvestigacion: new Date().toISOString(),
|
||||
});
|
||||
setInvestigateModal(null);
|
||||
};
|
||||
|
||||
const handleClose = async (id: string, observaciones?: string) => {
|
||||
await closeMutation.mutateAsync({
|
||||
id,
|
||||
fechaCierre: new Date().toISOString(),
|
||||
observaciones,
|
||||
});
|
||||
setCloseModal(null);
|
||||
};
|
||||
|
||||
const incidentes = data?.items || [];
|
||||
const fraccionamientos = fraccionamientosData?.items || [];
|
||||
|
||||
// Generate folio for display (assuming incremental numbering)
|
||||
const generateFolio = (index: number) => {
|
||||
const year = new Date().getFullYear();
|
||||
return `INC-${year}-${String(index + 1).padStart(4, '0')}`;
|
||||
};
|
||||
|
||||
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>
|
||||
<p className="text-gray-600">Gestión de incidentes de seguridad, salud y medio ambiente</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" />
|
||||
Registrar Incidente
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<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 folio o descripción..."
|
||||
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={fraccionamientoFilter}
|
||||
onChange={(e) => setFraccionamientoFilter(e.target.value)}
|
||||
>
|
||||
<option value="">Todos los fraccionamientos</option>
|
||||
{fraccionamientos.map((frac) => (
|
||||
<option key={frac.id} value={frac.id}>
|
||||
{frac.nombre}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<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 TipoIncidente | '')}
|
||||
>
|
||||
<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={gravedadFilter}
|
||||
onChange={(e) => setGravedadFilter(e.target.value as GravedadIncidente | '')}
|
||||
>
|
||||
<option value="">Todas las gravedades</option>
|
||||
{Object.entries(gravedadLabels).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={estadoFilter}
|
||||
onChange={(e) => setEstadoFilter(e.target.value as EstadoIncidente | '')}
|
||||
>
|
||||
<option value="">Todos los estados</option>
|
||||
{Object.entries(estadoLabels).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="date"
|
||||
className="px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
placeholder="Desde"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
className="px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
placeholder="Hasta"
|
||||
/>
|
||||
</div>
|
||||
</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" />
|
||||
{/* 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>
|
||||
) : incidentes.length === 0 ? (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
No hay incidentes registrados
|
||||
</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>
|
||||
) : (
|
||||
<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">
|
||||
Folio
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Fecha/Hora
|
||||
</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">
|
||||
Gravedad
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
||||
Descripción
|
||||
</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">
|
||||
{incidentes.map((item, index) => (
|
||||
<tr key={item.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{generateFolio(index)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
<div>
|
||||
{new Date(item.fecha).toLocaleDateString()}
|
||||
{item.hora && (
|
||||
<div className="text-xs text-gray-500">{item.hora}</div>
|
||||
)}
|
||||
</div>
|
||||
</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">
|
||||
<span
|
||||
className={clsx(
|
||||
'px-2 py-1 text-xs font-medium rounded-full',
|
||||
gravedadColors[item.gravedad]
|
||||
)}
|
||||
>
|
||||
{gravedadLabels[item.gravedad]}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 max-w-xs truncate">
|
||||
{item.descripcion}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={clsx(
|
||||
'px-2 py-1 text-xs font-medium rounded-full',
|
||||
estadoColors[item.estado]
|
||||
)}
|
||||
>
|
||||
{estadoLabels[item.estado]}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Link
|
||||
to={`/admin/hse/incidentes/${item.id}`}
|
||||
className="p-2 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg"
|
||||
title="Ver detalle"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Link>
|
||||
<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>
|
||||
{item.estado === 'abierto' && (
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-purple-600 hover:bg-purple-50 rounded-lg"
|
||||
title="Investigar"
|
||||
onClick={() => setInvestigateModal(item)}
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{item.estado === 'en_investigacion' && (
|
||||
<button
|
||||
className="p-2 text-gray-500 hover:text-green-600 hover:bg-green-50 rounded-lg"
|
||||
title="Cerrar"
|
||||
onClick={() => setCloseModal(item)}
|
||||
>
|
||||
<XCircle className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
{showModal && (
|
||||
<IncidenteModal
|
||||
item={editingItem}
|
||||
fraccionamientos={fraccionamientos}
|
||||
onClose={() => {
|
||||
setShowModal(false);
|
||||
setEditingItem(null);
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
isLoading={createMutation.isPending || updateMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Investigate Modal */}
|
||||
{investigateModal && (
|
||||
<InvestigateModal
|
||||
incidente={investigateModal}
|
||||
onClose={() => setInvestigateModal(null)}
|
||||
onSubmit={(investigadorId) => handleInvestigate(investigateModal.id, investigadorId)}
|
||||
isLoading={investigateMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Close Modal */}
|
||||
{closeModal && (
|
||||
<CloseModal
|
||||
incidente={closeModal}
|
||||
onClose={() => setCloseModal(null)}
|
||||
onSubmit={(observaciones) => handleClose(closeModal.id, observaciones)}
|
||||
isLoading={closeMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Modal Component
|
||||
interface IncidenteModalProps {
|
||||
item: Incidente | null;
|
||||
fraccionamientos: Fraccionamiento[];
|
||||
onClose: () => void;
|
||||
onSubmit: (data: CreateIncidenteDto) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function IncidenteModal({
|
||||
item,
|
||||
fraccionamientos,
|
||||
onClose,
|
||||
onSubmit,
|
||||
isLoading,
|
||||
}: IncidenteModalProps) {
|
||||
const [formData, setFormData] = useState<CreateIncidenteDto>({
|
||||
fraccionamientoId: item?.fraccionamientoId || '',
|
||||
tipo: item?.tipo || 'incidente',
|
||||
gravedad: item?.gravedad || 'leve',
|
||||
fecha: item?.fecha?.split('T')[0] || new Date().toISOString().split('T')[0],
|
||||
hora: item?.hora || '',
|
||||
ubicacion: item?.ubicacion || '',
|
||||
descripcion: item?.descripcion || '',
|
||||
causas: item?.causas || '',
|
||||
acciones: item?.acciones || '',
|
||||
observaciones: item?.observaciones || '',
|
||||
});
|
||||
|
||||
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 Incidente' : 'Registrar Nuevo Incidente'}
|
||||
</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">
|
||||
Fecha *
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
required
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
value={formData.fecha}
|
||||
onChange={(e) => setFormData({ ...formData, fecha: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Hora</label>
|
||||
<input
|
||||
type="time"
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
value={formData.hora}
|
||||
onChange={(e) => setFormData({ ...formData, hora: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Fraccionamiento *
|
||||
</label>
|
||||
<select
|
||||
required
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
value={formData.fraccionamientoId}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, fraccionamientoId: e.target.value })
|
||||
}
|
||||
>
|
||||
<option value="">Seleccione un fraccionamiento</option>
|
||||
{fraccionamientos.map((frac) => (
|
||||
<option key={frac.id} value={frac.id}>
|
||||
{frac.nombre}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<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 TipoIncidente })
|
||||
}
|
||||
>
|
||||
{Object.entries(tipoLabels).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Gravedad *
|
||||
</label>
|
||||
<select
|
||||
required
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
value={formData.gravedad}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, gravedad: e.target.value as GravedadIncidente })
|
||||
}
|
||||
>
|
||||
{Object.entries(gravedadLabels).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">
|
||||
Ubicación (descripción)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Ej: Zona de excavación, Área de cimbra, etc."
|
||||
value={formData.ubicacion}
|
||||
onChange={(e) => setFormData({ ...formData, ubicacion: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Descripción Detallada *
|
||||
</label>
|
||||
<textarea
|
||||
required
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Describa detalladamente lo sucedido..."
|
||||
value={formData.descripcion}
|
||||
onChange={(e) => setFormData({ ...formData, descripcion: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Causa Inmediata
|
||||
</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="¿Qué causó directamente el incidente?"
|
||||
value={formData.causas}
|
||||
onChange={(e) => setFormData({ ...formData, causas: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Acciones Inmediatas Tomadas
|
||||
</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="¿Qué acciones se tomaron de inmediato?"
|
||||
value={formData.acciones}
|
||||
onChange={(e) => setFormData({ ...formData, acciones: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Observaciones
|
||||
</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Información adicional..."
|
||||
value={formData.observaciones}
|
||||
onChange={(e) => setFormData({ ...formData, observaciones: e.target.value })}
|
||||
/>
|
||||
</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' : 'Registrar'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Investigate Modal
|
||||
interface InvestigateModalProps {
|
||||
incidente: Incidente;
|
||||
onClose: () => void;
|
||||
onSubmit: (investigadorId: string) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function InvestigateModal({ onClose, onSubmit, isLoading }: InvestigateModalProps) {
|
||||
const [investigadorId, setInvestigadorId] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
await onSubmit(investigadorId);
|
||||
};
|
||||
|
||||
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-md w-full mx-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<AlertTriangle className="w-6 h-6 text-purple-600" />
|
||||
<h3 className="text-lg font-semibold">Iniciar Investigación</h3>
|
||||
</div>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Asigne un investigador responsable para iniciar el proceso de investigación del
|
||||
incidente.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Investigador Responsable *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="ID del investigador"
|
||||
value={investigadorId}
|
||||
onChange={(e) => setInvestigadorId(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<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-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Procesando...' : 'Iniciar Investigación'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Close Modal
|
||||
interface CloseModalProps {
|
||||
incidente: Incidente;
|
||||
onClose: () => void;
|
||||
onSubmit: (observaciones?: string) => Promise<void>;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function CloseModal({ onClose, onSubmit, isLoading }: CloseModalProps) {
|
||||
const [observaciones, setObservaciones] = useState('');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
await onSubmit(observaciones || undefined);
|
||||
};
|
||||
|
||||
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-md w-full mx-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Activity className="w-6 h-6 text-green-600" />
|
||||
<h3 className="text-lg font-semibold">Cerrar Incidente</h3>
|
||||
</div>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Complete la investigación y cierre el incidente. Agregue observaciones finales si es
|
||||
necesario.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Observaciones Finales
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Resumen de la investigación y conclusiones..."
|
||||
value={observaciones}
|
||||
onChange={(e) => setObservaciones(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3">
|
||||
<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-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? 'Cerrando...' : 'Cerrar Incidente'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user