diff --git a/web/src/pages/admin/hse/InspeccionDetailPage.tsx b/web/src/pages/admin/hse/InspeccionDetailPage.tsx new file mode 100644 index 0000000..c5cafd5 --- /dev/null +++ b/web/src/pages/admin/hse/InspeccionDetailPage.tsx @@ -0,0 +1,725 @@ +import { useState } from 'react'; +import { useParams, useNavigate, Link } from 'react-router-dom'; +import { + ArrowLeft, + Plus, + CheckCircle, + XCircle, + MinusCircle, + AlertTriangle, + Eye, + Calendar, + User, + FileText, + ClipboardCheck, + AlertOctagon, +} from 'lucide-react'; +import { + useInspeccion, + useAddHallazgo, + useUpdateEstadoInspeccion, +} from '../../../hooks/useHSE'; +import { + Hallazgo, + CreateHallazgoDto, + GravedadHallazgo, +} from '../../../services/hse/inspecciones.api'; +import { HierarchyBreadcrumb } from '../../../components/proyectos'; +import clsx from 'clsx'; + +const estadoColors: Record = { + borrador: 'bg-gray-100 text-gray-800', + en_proceso: 'bg-blue-100 text-blue-800', + completada: 'bg-green-100 text-green-800', + cancelada: 'bg-red-100 text-red-800', +}; + +const estadoLabels: Record = { + borrador: 'Borrador', + en_proceso: 'En Proceso', + completada: 'Completada', + cancelada: 'Cancelada', +}; + +const gravedadColors: Record = { + baja: 'bg-green-100 text-green-800', + media: 'bg-yellow-100 text-yellow-800', + alta: 'bg-orange-100 text-orange-800', + critica: 'bg-red-100 text-red-800', +}; + +const gravedadLabels: Record = { + baja: 'Baja', + media: 'Media', + alta: 'Alta', + critica: 'Critica', +}; + +const hallazgoEstadoColors: Record = { + abierto: 'bg-red-100 text-red-800', + en_correccion: 'bg-yellow-100 text-yellow-800', + verificando: 'bg-blue-100 text-blue-800', + cerrado: 'bg-green-100 text-green-800', +}; + +const hallazgoEstadoLabels: Record = { + abierto: 'Abierto', + en_correccion: 'En Correccion', + verificando: 'Verificando', + cerrado: 'Cerrado', +}; + +const resultadoIcons = { + cumple: CheckCircle, + no_cumple: XCircle, + no_aplica: MinusCircle, +}; + +const resultadoColors = { + cumple: 'text-green-600', + no_cumple: 'text-red-600', + no_aplica: 'text-gray-400', +}; + +const resultadoLabels = { + cumple: 'Cumple', + no_cumple: 'No Cumple', + no_aplica: 'No Aplica', +}; + +interface Evaluacion { + id: string; + itemChecklist: string; + resultado: 'cumple' | 'no_cumple' | 'no_aplica'; + observaciones?: string; + tieneHallazgo?: boolean; +} + +export function InspeccionDetailPage() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [showHallazgoModal, setShowHallazgoModal] = useState(false); + + const { data: inspeccion, isLoading } = useInspeccion(id!); + const addHallazgoMutation = useAddHallazgo(); + const updateEstadoMutation = useUpdateEstadoInspeccion(); + + const handleAddHallazgo = async (data: CreateHallazgoDto) => { + await addHallazgoMutation.mutateAsync({ id: id!, data }); + setShowHallazgoModal(false); + }; + + const handleCompletarInspeccion = async () => { + if (window.confirm('¿Está seguro de marcar esta inspección como completada?')) { + await updateEstadoMutation.mutateAsync({ + id: id!, + data: { + estado: 'completada', + observaciones: 'Inspección completada', + }, + }); + } + }; + + if (isLoading) { + return
Cargando...
; + } + + if (!inspeccion) { + return
Inspección no encontrada
; + } + + const evaluaciones: Evaluacion[] = [ + { + id: '1', + itemChecklist: 'Uso correcto de EPP por parte del personal', + resultado: 'cumple', + observaciones: 'Todo el personal porta casco y chaleco', + }, + { + id: '2', + itemChecklist: 'Señalización de zonas de riesgo', + resultado: 'no_cumple', + observaciones: 'Falta señalización en zona de excavación', + tieneHallazgo: true, + }, + { + id: '3', + itemChecklist: 'Orden y limpieza en área de trabajo', + resultado: 'cumple', + }, + { + id: '4', + itemChecklist: 'Equipo de extinción de incendios', + resultado: 'no_aplica', + observaciones: 'No aplica para esta área', + }, + { + id: '5', + itemChecklist: 'Protección de bordes y excavaciones', + resultado: 'no_cumple', + observaciones: 'Excavación sin barandal de protección', + tieneHallazgo: true, + }, + { + id: '6', + itemChecklist: 'Almacenamiento adecuado de materiales', + resultado: 'cumple', + }, + ]; + + const hallazgos = inspeccion.hallazgos || []; + + const totalEvaluaciones = evaluaciones.length; + const cumple = evaluaciones.filter((e) => e.resultado === 'cumple').length; + const noCumple = evaluaciones.filter((e) => e.resultado === 'no_cumple').length; + const noAplica = evaluaciones.filter((e) => e.resultado === 'no_aplica').length; + const porcentajeCumplimiento = totalEvaluaciones > 0 + ? Math.round((cumple / (totalEvaluaciones - noAplica)) * 100) + : 0; + + const hallazgosAbiertos = hallazgos.filter((h) => !h.cerrado).length; + + const tipoInspeccion = 'Seguridad General'; + const fraccionamiento = 'Residencial Los Pinos'; + const inspector = 'Juan Pérez García'; + + return ( +
+ + + {/* Header */} +
+
+
+
+

+ Inspección: {tipoInspeccion} - {fraccionamiento} +

+ + {estadoLabels[inspeccion.estado]} + +
+

+ Folio: INS-{new Date(inspeccion.fecha).getFullYear()}- + {String(Math.floor(Math.random() * 1000)).padStart(4, '0')} +

+
+
+ + {inspeccion.estado === 'en_proceso' && ( + <> + + + + )} +
+
+
+ + {/* Información General */} +
+
+ +

Información General

+
+ +
+
+ +

{tipoInspeccion}

+
+
+ +

{fraccionamiento}

+
+
+ +

{inspector}

+
+
+ +

+ {new Date(inspeccion.fecha).toLocaleDateString('es-MX', { + year: 'numeric', + month: 'long', + day: 'numeric', + })} + {inspeccion.hora && ( + {inspeccion.hora} + )} +

+
+
+ + {/* Barra de cumplimiento */} +
+
+ + + {porcentajeCumplimiento}% + +
+
+
= 90 + ? 'bg-green-500' + : porcentajeCumplimiento >= 70 + ? 'bg-yellow-500' + : 'bg-red-500' + )} + style={{ width: `${porcentajeCumplimiento}%` }} + > +
+ {porcentajeCumplimiento >= 15 && `${porcentajeCumplimiento}%`} +
+
+
+
+ + {/* Contadores de items */} +
+
+
+
+

Cumple

+

{cumple}

+
+ +
+
+
+
+
+

No Cumple

+

{noCumple}

+
+ +
+
+
+
+
+

No Aplica

+

{noAplica}

+
+ +
+
+
+ + {/* Observaciones generales */} + {inspeccion.observaciones && ( +
+ +

+ {inspeccion.observaciones} +

+
+ )} +
+ + {/* Evaluaciones del Checklist */} +
+
+
+ +

+ Evaluaciones del Checklist +

+
+ + {totalEvaluaciones} items evaluados + +
+ +
+ {evaluaciones.map((evaluacion) => { + const ResultadoIcon = resultadoIcons[evaluacion.resultado]; + return ( +
+
+ +
+

{evaluacion.itemChecklist}

+ {evaluacion.observaciones && ( +

{evaluacion.observaciones}

+ )} + {evaluacion.tieneHallazgo && ( +
+ + Tiene hallazgo asociado +
+ )} +
+ + {resultadoLabels[evaluacion.resultado]} + +
+
+ ); + })} +
+
+ + {/* Hallazgos */} +
+
+
+ +

Hallazgos Identificados

+ {hallazgosAbiertos > 0 && ( + + {hallazgosAbiertos} abiertos + + )} +
+ {hallazgos.length} hallazgos +
+ + {hallazgos.length === 0 ? ( +
+ +

No se han registrado hallazgos en esta inspección

+ {inspeccion.estado === 'en_proceso' && ( + + )} +
+ ) : ( +
+ {hallazgos.map((hallazgo) => ( + + ))} +
+ )} +
+ + {/* Modal Agregar Hallazgo */} + {showHallazgoModal && ( + setShowHallazgoModal(false)} + onSubmit={handleAddHallazgo} + isLoading={addHallazgoMutation.isPending} + /> + )} +
+ ); +} + +interface HallazgoCardProps { + hallazgo: Hallazgo; +} + +function HallazgoCard({ hallazgo }: HallazgoCardProps) { + const [expanded, setExpanded] = useState(false); + + const estado = hallazgo.cerrado ? 'cerrado' : 'abierto'; + const folio = `HAL-${new Date(hallazgo.createdAt).getFullYear()}-${String( + Math.floor(Math.random() * 1000) + ).padStart(4, '0')}`; + + return ( +
+
+
+
+ {folio} + + {gravedadLabels[hallazgo.gravedad]} + + + {hallazgoEstadoLabels[estado]} + +
+ +

{hallazgo.item}

+

{hallazgo.descripcion}

+ + {expanded && ( +
+ {hallazgo.accionCorrectiva && ( +
+ +

{hallazgo.accionCorrectiva}

+
+ )} + {hallazgo.responsableId && ( +
+ +

+ Usuario ID: {hallazgo.responsableId} +

+
+ )} + {hallazgo.fechaCompromiso && ( +
+ +

+ {new Date(hallazgo.fechaCompromiso).toLocaleDateString('es-MX')} +

+
+ )} + {hallazgo.fechaCierre && ( +
+ +

+ {new Date(hallazgo.fechaCierre).toLocaleDateString('es-MX')} +

+
+ )} +
+ )} + + +
+ + {!hallazgo.cerrado && ( +
+ +
+ )} +
+
+ ); +} + +interface HallazgoModalProps { + onClose: () => void; + onSubmit: (data: CreateHallazgoDto) => Promise; + isLoading: boolean; +} + +function HallazgoModal({ onClose, onSubmit, isLoading }: HallazgoModalProps) { + const [formData, setFormData] = useState({ + item: '', + descripcion: '', + gravedad: 'media', + accionCorrectiva: '', + responsableId: '', + fechaCompromiso: '', + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + await onSubmit(formData); + }; + + return ( +
+
+
+ +

Registrar Nuevo Hallazgo

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