# ET-SEG-001: Especificaciones Técnicas - Frontend Seguridad Industrial ## 1. Información General | Campo | Valor | |-------|-------| | **Módulo** | MAI-007 - Seguridad Industrial | | **Vertical** | Construcción | | **Documento** | ET-SEG-001-frontend | | **Versión** | 1.0.0 | | **Fecha** | 2025-12-06 | | **Responsable** | Equipo Frontend | ## 2. Stack Tecnológico ### 2.1 Core Technologies ```json { "runtime": "React 18", "buildTool": "Vite 5.x", "language": "TypeScript 5.x", "stateManagement": "Zustand 4.x", "routing": "React Router 6.x" } ``` ### 2.2 Librerías Principales | Librería | Versión | Propósito | |----------|---------|-----------| | `react` | ^18.2.0 | Framework UI | | `react-dom` | ^18.2.0 | DOM rendering | | `vite` | ^5.0.0 | Build tool | | `zustand` | ^4.5.0 | State management | | `react-router-dom` | ^6.22.0 | Routing | | `@fullcalendar/react` | ^6.1.0 | Calendario capacitaciones | | `@fullcalendar/daygrid` | ^6.1.0 | Vista mensual | | `@fullcalendar/timegrid` | ^6.1.0 | Vista semanal | | `@fullcalendar/interaction` | ^6.1.0 | Interacciones | | `react-hook-form` | ^7.51.0 | Gestión de formularios | | `zod` | ^3.22.0 | Validación schemas | | `date-fns` | ^3.3.0 | Manipulación fechas | | `recharts` | ^2.12.0 | Gráficos KPIs | | `lucide-react` | ^0.344.0 | Iconos | ## 3. Arquitectura Frontend ### 3.1 Estructura de Directorios ``` apps/verticales/construccion/frontend/src/modules/safety/ ├── features/ │ ├── epp/ │ │ ├── components/ │ │ │ ├── EPPAssignmentForm.tsx │ │ │ ├── EPPInventoryList.tsx │ │ │ ├── EPPAssignmentHistory.tsx │ │ │ └── EPPExpirationAlerts.tsx │ │ ├── hooks/ │ │ │ ├── useEPPAssignment.ts │ │ │ ├── useEPPInventory.ts │ │ │ └── useEPPValidations.ts │ │ ├── stores/ │ │ │ └── eppStore.ts │ │ ├── types/ │ │ │ └── epp.types.ts │ │ └── utils/ │ │ └── eppValidations.ts │ │ │ ├── safety-inspections/ │ │ ├── components/ │ │ │ ├── SafetyChecklist.tsx │ │ │ ├── ChecklistTemplateBuilder.tsx │ │ │ ├── InspectionForm.tsx │ │ │ ├── InspectionResults.tsx │ │ │ └── InspectionHistory.tsx │ │ ├── hooks/ │ │ │ ├── useSafetyInspection.ts │ │ │ ├── useChecklist.ts │ │ │ └── useInspectionValidations.ts │ │ ├── stores/ │ │ │ └── inspectionStore.ts │ │ ├── types/ │ │ │ └── inspection.types.ts │ │ └── utils/ │ │ └── checklistCalculations.ts │ │ │ ├── incidents/ │ │ ├── components/ │ │ │ ├── IncidentReportForm.tsx │ │ │ ├── IncidentTimeline.tsx │ │ │ ├── IncidentDetails.tsx │ │ │ ├── IncidentPhotos.tsx │ │ │ ├── IncidentWitnesses.tsx │ │ │ └── IncidentCorrectiveActions.tsx │ │ ├── hooks/ │ │ │ ├── useIncidentReport.ts │ │ │ ├── useIncidentTimeline.ts │ │ │ └── useIncidentAnalysis.ts │ │ ├── stores/ │ │ │ └── incidentStore.ts │ │ ├── types/ │ │ │ └── incident.types.ts │ │ └── utils/ │ │ └── incidentSeverityCalculator.ts │ │ │ └── trainings/ │ ├── components/ │ │ ├── TrainingCalendar.tsx │ │ ├── TrainingMatrix.tsx │ │ ├── TrainingScheduleForm.tsx │ │ ├── TrainingAttendance.tsx │ │ ├── TrainingCertificates.tsx │ │ └── TrainingReminders.tsx │ ├── hooks/ │ │ ├── useTrainingCalendar.ts │ │ ├── useTrainingMatrix.ts │ │ └── useTrainingCertifications.ts │ ├── stores/ │ │ └── trainingStore.ts │ ├── types/ │ │ └── training.types.ts │ └── utils/ │ └── certificationValidator.ts │ ├── dashboard/ │ ├── components/ │ │ ├── SafetyDashboard.tsx │ │ ├── IncidentRateKPI.tsx │ │ ├── ComplianceKPI.tsx │ │ ├── EPPComplianceChart.tsx │ │ ├── TrainingCompletionChart.tsx │ │ ├── IncidentTrendChart.tsx │ │ └── SafetyAlerts.tsx │ ├── hooks/ │ │ └── useSafetyMetrics.ts │ └── stores/ │ └── dashboardStore.ts │ ├── shared/ │ ├── components/ │ │ ├── SeverityBadge.tsx │ │ ├── StatusIndicator.tsx │ │ └── SafetyIcon.tsx │ ├── constants/ │ │ └── safetyConstants.ts │ └── utils/ │ ├── dateHelpers.ts │ └── formatters.ts │ └── routes/ └── safetyRoutes.tsx ``` ## 4. Feature Modules ### 4.1 EPP (Equipo de Protección Personal) #### 4.1.1 EPPAssignmentForm Component ```typescript // apps/verticales/construccion/frontend/src/modules/safety/features/epp/components/EPPAssignmentForm.tsx import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useEPPAssignment } from '../hooks/useEPPAssignment'; const eppAssignmentSchema = z.object({ employeeId: z.string().min(1, 'Empleado requerido'), projectId: z.string().min(1, 'Proyecto requerido'), eppItems: z.array(z.object({ eppId: z.string(), quantity: z.number().min(1), size: z.string().optional(), expirationDate: z.date().optional(), serialNumber: z.string().optional() })).min(1, 'Debe asignar al menos un EPP'), assignmentDate: z.date(), notes: z.string().optional() }); type EPPAssignmentFormData = z.infer; export const EPPAssignmentForm: React.FC = () => { const { assignEPP, isLoading } = useEPPAssignment(); const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(eppAssignmentSchema), defaultValues: { assignmentDate: new Date(), eppItems: [] } }); const onSubmit = async (data: EPPAssignmentFormData) => { await assignEPP(data); }; return (
{/* Form implementation */}
); }; ``` #### 4.1.2 EPP Store ```typescript // apps/verticales/construccion/frontend/src/modules/safety/features/epp/stores/eppStore.ts import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; interface EPPItem { id: string; name: string; category: string; stock: number; minStock: number; cost: number; } interface EPPAssignment { id: string; employeeId: string; employeeName: string; projectId: string; items: Array<{ eppId: string; eppName: string; quantity: number; assignmentDate: string; expirationDate?: string; }>; status: 'active' | 'returned' | 'expired'; } interface EPPStore { // State inventory: EPPItem[]; assignments: EPPAssignment[]; selectedAssignment: EPPAssignment | null; isLoading: boolean; error: string | null; // Actions fetchInventory: () => Promise; fetchAssignments: (filters?: Record) => Promise; createAssignment: (data: any) => Promise; updateAssignment: (id: string, data: any) => Promise; returnEPP: (assignmentId: string, items: string[]) => Promise; setSelectedAssignment: (assignment: EPPAssignment | null) => void; checkExpiredEPP: () => EPPAssignment[]; checkLowStock: () => EPPItem[]; } export const useEPPStore = create()( devtools( persist( (set, get) => ({ // Initial state inventory: [], assignments: [], selectedAssignment: null, isLoading: false, error: null, // Actions fetchInventory: async () => { set({ isLoading: true, error: null }); try { const response = await fetch('/api/safety/epp/inventory'); const data = await response.json(); set({ inventory: data, isLoading: false }); } catch (error) { set({ error: error.message, isLoading: false }); } }, fetchAssignments: async (filters = {}) => { set({ isLoading: true, error: null }); try { const queryParams = new URLSearchParams(filters); const response = await fetch(`/api/safety/epp/assignments?${queryParams}`); const data = await response.json(); set({ assignments: data, isLoading: false }); } catch (error) { set({ error: error.message, isLoading: false }); } }, createAssignment: async (data) => { set({ isLoading: true, error: null }); try { const response = await fetch('/api/safety/epp/assignments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const newAssignment = await response.json(); set(state => ({ assignments: [...state.assignments, newAssignment], isLoading: false })); } catch (error) { set({ error: error.message, isLoading: false }); } }, updateAssignment: async (id, data) => { set({ isLoading: true, error: null }); try { const response = await fetch(`/api/safety/epp/assignments/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const updated = await response.json(); set(state => ({ assignments: state.assignments.map(a => a.id === id ? updated : a), isLoading: false })); } catch (error) { set({ error: error.message, isLoading: false }); } }, returnEPP: async (assignmentId, items) => { set({ isLoading: true, error: null }); try { await fetch(`/api/safety/epp/assignments/${assignmentId}/return`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items }) }); await get().fetchAssignments(); } catch (error) { set({ error: error.message, isLoading: false }); } }, setSelectedAssignment: (assignment) => set({ selectedAssignment: assignment }), checkExpiredEPP: () => { const now = new Date(); return get().assignments.filter(assignment => assignment.items.some(item => item.expirationDate && new Date(item.expirationDate) <= now ) ); }, checkLowStock: () => { return get().inventory.filter(item => item.stock <= item.minStock); } }), { name: 'epp-storage', partialize: (state) => ({ selectedAssignment: state.selectedAssignment }) } ) ) ); ``` ### 4.2 Safety Inspections #### 4.2.1 SafetyChecklist Component ```typescript // apps/verticales/construccion/frontend/src/modules/safety/features/safety-inspections/components/SafetyChecklist.tsx import React, { useState } from 'react'; import { Check, X, AlertTriangle } from 'lucide-react'; import { useChecklist } from '../hooks/useChecklist'; interface ChecklistItem { id: string; category: string; description: string; required: boolean; status?: 'compliant' | 'non-compliant' | 'not-applicable'; observations?: string; photos?: string[]; } interface SafetyChecklistProps { templateId: string; projectId: string; inspectionDate: Date; onSubmit: (data: any) => void; } export const SafetyChecklist: React.FC = ({ templateId, projectId, inspectionDate, onSubmit }) => { const { template, isLoading } = useChecklist(templateId); const [items, setItems] = useState([]); const [observations, setObservations] = useState>({}); const handleItemChange = (itemId: string, status: ChecklistItem['status']) => { setItems(prev => prev.map(item => item.id === itemId ? { ...item, status } : item ) ); }; const handleObservationChange = (itemId: string, observation: string) => { setObservations(prev => ({ ...prev, [itemId]: observation })); }; const calculateCompliance = () => { const total = items.filter(item => item.required).length; const compliant = items.filter( item => item.required && item.status === 'compliant' ).length; return total > 0 ? (compliant / total) * 100 : 0; }; const handleSubmit = () => { const inspectionData = { templateId, projectId, inspectionDate, items: items.map(item => ({ ...item, observations: observations[item.id] })), compliancePercentage: calculateCompliance(), completedBy: 'current-user-id', // From auth context completedAt: new Date() }; onSubmit(inspectionData); }; if (isLoading) return
Cargando checklist...
; return (

{template?.name}

Cumplimiento: {calculateCompliance().toFixed(1)}%
{template?.categories.map(category => (

{category.name}

{category.items.map(item => (
{item.description} {item.required && *}
{items.find(i => i.id === item.id)?.status === 'non-compliant' && (