erp-construccion/docs/02-definicion-modulos/MAI-007-seguridad-industrial/especificaciones/ET-SEG-001-frontend.md

59 KiB

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

{
  "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

// 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<typeof eppAssignmentSchema>;

export const EPPAssignmentForm: React.FC = () => {
  const { assignEPP, isLoading } = useEPPAssignment();

  const { register, handleSubmit, formState: { errors } } = useForm<EPPAssignmentFormData>({
    resolver: zodResolver(eppAssignmentSchema),
    defaultValues: {
      assignmentDate: new Date(),
      eppItems: []
    }
  });

  const onSubmit = async (data: EPPAssignmentFormData) => {
    await assignEPP(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Form implementation */}
    </form>
  );
};

4.1.2 EPP Store

// 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<void>;
  fetchAssignments: (filters?: Record<string, any>) => Promise<void>;
  createAssignment: (data: any) => Promise<void>;
  updateAssignment: (id: string, data: any) => Promise<void>;
  returnEPP: (assignmentId: string, items: string[]) => Promise<void>;
  setSelectedAssignment: (assignment: EPPAssignment | null) => void;
  checkExpiredEPP: () => EPPAssignment[];
  checkLowStock: () => EPPItem[];
}

export const useEPPStore = create<EPPStore>()(
  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

// 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<SafetyChecklistProps> = ({
  templateId,
  projectId,
  inspectionDate,
  onSubmit
}) => {
  const { template, isLoading } = useChecklist(templateId);
  const [items, setItems] = useState<ChecklistItem[]>([]);
  const [observations, setObservations] = useState<Record<string, string>>({});

  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 <div>Cargando checklist...</div>;

  return (
    <div className="safety-checklist">
      <div className="checklist-header">
        <h2>{template?.name}</h2>
        <div className="compliance-score">
          Cumplimiento: {calculateCompliance().toFixed(1)}%
        </div>
      </div>

      {template?.categories.map(category => (
        <div key={category.id} className="checklist-category">
          <h3>{category.name}</h3>
          {category.items.map(item => (
            <div key={item.id} className="checklist-item">
              <div className="item-description">
                {item.description}
                {item.required && <span className="required">*</span>}
              </div>

              <div className="item-controls">
                <button
                  className={`status-btn ${items.find(i => i.id === item.id)?.status === 'compliant' ? 'active' : ''}`}
                  onClick={() => handleItemChange(item.id, 'compliant')}
                >
                  <Check size={20} /> Conforme
                </button>
                <button
                  className={`status-btn ${items.find(i => i.id === item.id)?.status === 'non-compliant' ? 'active' : ''}`}
                  onClick={() => handleItemChange(item.id, 'non-compliant')}
                >
                  <X size={20} /> No Conforme
                </button>
                <button
                  className={`status-btn ${items.find(i => i.id === item.id)?.status === 'not-applicable' ? 'active' : ''}`}
                  onClick={() => handleItemChange(item.id, 'not-applicable')}
                >
                  N/A
                </button>
              </div>

              {items.find(i => i.id === item.id)?.status === 'non-compliant' && (
                <textarea
                  placeholder="Observaciones sobre el incumplimiento..."
                  value={observations[item.id] || ''}
                  onChange={(e) => handleObservationChange(item.id, e.target.value)}
                  className="observations-input"
                />
              )}
            </div>
          ))}
        </div>
      ))}

      <button onClick={handleSubmit} className="submit-btn">
        Finalizar Inspección
      </button>
    </div>
  );
};

4.2.2 Inspection Store

// apps/verticales/construccion/frontend/src/modules/safety/features/safety-inspections/stores/inspectionStore.ts

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

interface ChecklistTemplate {
  id: string;
  name: string;
  description: string;
  categories: Array<{
    id: string;
    name: string;
    items: Array<{
      id: string;
      description: string;
      required: boolean;
    }>;
  }>;
}

interface Inspection {
  id: string;
  projectId: string;
  templateId: string;
  inspectionDate: string;
  compliancePercentage: number;
  status: 'draft' | 'completed' | 'requires-followup';
  completedBy: string;
  items: any[];
}

interface InspectionStore {
  templates: ChecklistTemplate[];
  inspections: Inspection[];
  selectedInspection: Inspection | null;
  isLoading: boolean;
  error: string | null;

  fetchTemplates: () => Promise<void>;
  fetchInspections: (projectId?: string) => Promise<void>;
  createInspection: (data: any) => Promise<void>;
  updateInspection: (id: string, data: any) => Promise<void>;
  setSelectedInspection: (inspection: Inspection | null) => void;
  getComplianceTrend: (projectId: string) => Promise<any[]>;
}

export const useInspectionStore = create<InspectionStore>()(
  devtools((set, get) => ({
    templates: [],
    inspections: [],
    selectedInspection: null,
    isLoading: false,
    error: null,

    fetchTemplates: async () => {
      set({ isLoading: true, error: null });
      try {
        const response = await fetch('/api/safety/inspections/templates');
        const data = await response.json();
        set({ templates: data, isLoading: false });
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    fetchInspections: async (projectId) => {
      set({ isLoading: true, error: null });
      try {
        const url = projectId
          ? `/api/safety/inspections?projectId=${projectId}`
          : '/api/safety/inspections';
        const response = await fetch(url);
        const data = await response.json();
        set({ inspections: data, isLoading: false });
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    createInspection: async (data) => {
      set({ isLoading: true, error: null });
      try {
        const response = await fetch('/api/safety/inspections', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });
        const newInspection = await response.json();
        set(state => ({
          inspections: [...state.inspections, newInspection],
          isLoading: false
        }));
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    updateInspection: async (id, data) => {
      set({ isLoading: true, error: null });
      try {
        const response = await fetch(`/api/safety/inspections/${id}`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });
        const updated = await response.json();
        set(state => ({
          inspections: state.inspections.map(i => i.id === id ? updated : i),
          isLoading: false
        }));
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    setSelectedInspection: (inspection) => set({ selectedInspection: inspection }),

    getComplianceTrend: async (projectId) => {
      try {
        const response = await fetch(`/api/safety/inspections/compliance-trend?projectId=${projectId}`);
        return await response.json();
      } catch (error) {
        console.error('Error fetching compliance trend:', error);
        return [];
      }
    }
  }))
);

4.3 Incidents

4.3.1 IncidentReportForm Component

// apps/verticales/construccion/frontend/src/modules/safety/features/incidents/components/IncidentReportForm.tsx

import { useForm, useFieldArray } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useIncidentReport } from '../hooks/useIncidentReport';

const incidentSchema = z.object({
  projectId: z.string().min(1, 'Proyecto requerido'),
  incidentDate: z.date(),
  incidentTime: z.string(),
  location: z.string().min(1, 'Ubicación requerida'),
  type: z.enum(['accident', 'near-miss', 'property-damage', 'environmental']),
  severity: z.enum(['minor', 'moderate', 'serious', 'critical', 'fatal']),
  description: z.string().min(20, 'Descripción debe tener al menos 20 caracteres'),

  affectedPersons: z.array(z.object({
    employeeId: z.string().optional(),
    name: z.string().min(1),
    role: z.string(),
    injuryType: z.string().optional(),
    medicalAttention: z.boolean()
  })),

  witnesses: z.array(z.object({
    employeeId: z.string().optional(),
    name: z.string().min(1),
    contact: z.string(),
    statement: z.string()
  })),

  immediateActions: z.string(),
  rootCause: z.string().optional(),

  correctiveActions: z.array(z.object({
    description: z.string().min(1),
    responsible: z.string(),
    dueDate: z.date(),
    status: z.enum(['pending', 'in-progress', 'completed'])
  })),

  photos: z.array(z.string()).optional(),
  reportedBy: z.string()
});

type IncidentFormData = z.infer<typeof incidentSchema>;

export const IncidentReportForm: React.FC = () => {
  const { createIncident, isLoading } = useIncidentReport();

  const {
    register,
    control,
    handleSubmit,
    formState: { errors }
  } = useForm<IncidentFormData>({
    resolver: zodResolver(incidentSchema),
    defaultValues: {
      incidentDate: new Date(),
      affectedPersons: [],
      witnesses: [],
      correctiveActions: []
    }
  });

  const { fields: affectedFields, append: addAffected, remove: removeAffected } =
    useFieldArray({ control, name: 'affectedPersons' });

  const { fields: witnessFields, append: addWitness, remove: removeWitness } =
    useFieldArray({ control, name: 'witnesses' });

  const { fields: actionFields, append: addAction, remove: removeAction } =
    useFieldArray({ control, name: 'correctiveActions' });

  const onSubmit = async (data: IncidentFormData) => {
    await createIncident(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="incident-form">
      <section className="form-section">
        <h3>Información General</h3>
        {/* General info fields */}
      </section>

      <section className="form-section">
        <h3>Personas Afectadas</h3>
        {affectedFields.map((field, index) => (
          <div key={field.id} className="array-item">
            {/* Affected person fields */}
          </div>
        ))}
        <button type="button" onClick={() => addAffected({})}>
          Agregar Persona Afectada
        </button>
      </section>

      <section className="form-section">
        <h3>Testigos</h3>
        {witnessFields.map((field, index) => (
          <div key={field.id} className="array-item">
            {/* Witness fields */}
          </div>
        ))}
        <button type="button" onClick={() => addWitness({})}>
          Agregar Testigo
        </button>
      </section>

      <section className="form-section">
        <h3>Acciones Correctivas</h3>
        {actionFields.map((field, index) => (
          <div key={field.id} className="array-item">
            {/* Corrective action fields */}
          </div>
        ))}
        <button type="button" onClick={() => addAction({})}>
          Agregar Acción Correctiva
        </button>
      </section>

      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Guardando...' : 'Registrar Incidente'}
      </button>
    </form>
  );
};

4.3.2 IncidentTimeline Component

// apps/verticales/construccion/frontend/src/modules/safety/features/incidents/components/IncidentTimeline.tsx

import React from 'react';
import { format } from 'date-fns';
import { es } from 'date-fns/locale';
import { Clock, CheckCircle, AlertCircle, User } from 'lucide-react';

interface TimelineEvent {
  id: string;
  type: 'incident' | 'action' | 'update' | 'closure';
  timestamp: string;
  title: string;
  description: string;
  user: string;
  status?: string;
}

interface IncidentTimelineProps {
  incidentId: string;
  events: TimelineEvent[];
}

export const IncidentTimeline: React.FC<IncidentTimelineProps> = ({
  incidentId,
  events
}) => {
  const getIcon = (type: TimelineEvent['type']) => {
    switch (type) {
      case 'incident':
        return <AlertCircle className="text-red-500" />;
      case 'action':
        return <Clock className="text-blue-500" />;
      case 'update':
        return <User className="text-yellow-500" />;
      case 'closure':
        return <CheckCircle className="text-green-500" />;
      default:
        return <Clock />;
    }
  };

  return (
    <div className="incident-timeline">
      <h3>Línea de Tiempo del Incidente</h3>

      <div className="timeline-container">
        {events.map((event, index) => (
          <div key={event.id} className="timeline-item">
            <div className="timeline-marker">
              {getIcon(event.type)}
            </div>

            <div className="timeline-content">
              <div className="timeline-header">
                <h4>{event.title}</h4>
                <span className="timeline-date">
                  {format(new Date(event.timestamp), "dd MMM yyyy HH:mm", { locale: es })}
                </span>
              </div>

              <p className="timeline-description">{event.description}</p>

              <div className="timeline-meta">
                <span className="timeline-user">
                  <User size={14} /> {event.user}
                </span>
                {event.status && (
                  <span className={`timeline-status status-${event.status}`}>
                    {event.status}
                  </span>
                )}
              </div>
            </div>

            {index < events.length - 1 && <div className="timeline-connector" />}
          </div>
        ))}
      </div>
    </div>
  );
};

4.3.3 Incident Store

// apps/verticales/construccion/frontend/src/modules/safety/features/incidents/stores/incidentStore.ts

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

interface Incident {
  id: string;
  projectId: string;
  projectName: string;
  incidentDate: string;
  type: string;
  severity: string;
  description: string;
  status: 'reported' | 'investigating' | 'actions-pending' | 'closed';
  affectedPersons: any[];
  witnesses: any[];
  correctiveActions: any[];
  photos: string[];
  timeline: any[];
}

interface IncidentStore {
  incidents: Incident[];
  selectedIncident: Incident | null;
  filters: {
    projectId?: string;
    severity?: string;
    status?: string;
    dateFrom?: string;
    dateTo?: string;
  };
  isLoading: boolean;
  error: string | null;

  fetchIncidents: (filters?: any) => Promise<void>;
  getIncidentById: (id: string) => Promise<void>;
  createIncident: (data: any) => Promise<void>;
  updateIncident: (id: string, data: any) => Promise<void>;
  addCorrectiveAction: (incidentId: string, action: any) => Promise<void>;
  updateActionStatus: (incidentId: string, actionId: string, status: string) => Promise<void>;
  closeIncident: (incidentId: string) => Promise<void>;
  setFilters: (filters: any) => void;
  setSelectedIncident: (incident: Incident | null) => void;
  getIncidentRate: (projectId: string, period: string) => Promise<number>;
}

export const useIncidentStore = create<IncidentStore>()(
  devtools((set, get) => ({
    incidents: [],
    selectedIncident: null,
    filters: {},
    isLoading: false,
    error: null,

    fetchIncidents: async (filters = {}) => {
      set({ isLoading: true, error: null, filters });
      try {
        const queryParams = new URLSearchParams(filters);
        const response = await fetch(`/api/safety/incidents?${queryParams}`);
        const data = await response.json();
        set({ incidents: data, isLoading: false });
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    getIncidentById: async (id) => {
      set({ isLoading: true, error: null });
      try {
        const response = await fetch(`/api/safety/incidents/${id}`);
        const data = await response.json();
        set({ selectedIncident: data, isLoading: false });
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    createIncident: async (data) => {
      set({ isLoading: true, error: null });
      try {
        const response = await fetch('/api/safety/incidents', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });
        const newIncident = await response.json();
        set(state => ({
          incidents: [...state.incidents, newIncident],
          isLoading: false
        }));
        return newIncident;
      } catch (error) {
        set({ error: error.message, isLoading: false });
        throw error;
      }
    },

    updateIncident: async (id, data) => {
      set({ isLoading: true, error: null });
      try {
        const response = await fetch(`/api/safety/incidents/${id}`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });
        const updated = await response.json();
        set(state => ({
          incidents: state.incidents.map(i => i.id === id ? updated : i),
          selectedIncident: state.selectedIncident?.id === id ? updated : state.selectedIncident,
          isLoading: false
        }));
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    addCorrectiveAction: async (incidentId, action) => {
      set({ isLoading: true, error: null });
      try {
        const response = await fetch(`/api/safety/incidents/${incidentId}/actions`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(action)
        });
        await response.json();
        await get().getIncidentById(incidentId);
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    updateActionStatus: async (incidentId, actionId, status) => {
      try {
        await fetch(`/api/safety/incidents/${incidentId}/actions/${actionId}`, {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ status })
        });
        await get().getIncidentById(incidentId);
      } catch (error) {
        console.error('Error updating action status:', error);
      }
    },

    closeIncident: async (incidentId) => {
      set({ isLoading: true, error: null });
      try {
        await fetch(`/api/safety/incidents/${incidentId}/close`, {
          method: 'POST'
        });
        await get().getIncidentById(incidentId);
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    setFilters: (filters) => set({ filters }),

    setSelectedIncident: (incident) => set({ selectedIncident: incident }),

    getIncidentRate: async (projectId, period) => {
      try {
        const response = await fetch(
          `/api/safety/incidents/rate?projectId=${projectId}&period=${period}`
        );
        const data = await response.json();
        return data.rate;
      } catch (error) {
        console.error('Error fetching incident rate:', error);
        return 0;
      }
    }
  }))
);

4.4 Trainings

4.4.1 TrainingCalendar Component

// apps/verticales/construccion/frontend/src/modules/safety/features/trainings/components/TrainingCalendar.tsx

import React, { useEffect, useState } from 'react';
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import interactionPlugin from '@fullcalendar/interaction';
import esLocale from '@fullcalendar/core/locales/es';
import { useTrainingCalendar } from '../hooks/useTrainingCalendar';

interface TrainingCalendarProps {
  projectId?: string;
  onEventClick?: (event: any) => void;
  onDateClick?: (date: Date) => void;
}

export const TrainingCalendar: React.FC<TrainingCalendarProps> = ({
  projectId,
  onEventClick,
  onDateClick
}) => {
  const { trainings, fetchTrainings, isLoading } = useTrainingCalendar();
  const [events, setEvents] = useState([]);

  useEffect(() => {
    fetchTrainings(projectId);
  }, [projectId]);

  useEffect(() => {
    const calendarEvents = trainings.map(training => ({
      id: training.id,
      title: training.title,
      start: training.startDate,
      end: training.endDate,
      backgroundColor: getEventColor(training.status),
      borderColor: getEventColor(training.status),
      extendedProps: {
        instructor: training.instructor,
        capacity: training.capacity,
        enrolled: training.enrolled,
        status: training.status,
        location: training.location
      }
    }));
    setEvents(calendarEvents);
  }, [trainings]);

  const getEventColor = (status: string) => {
    const colors = {
      'scheduled': '#3b82f6',
      'in-progress': '#f59e0b',
      'completed': '#10b981',
      'cancelled': '#ef4444'
    };
    return colors[status] || '#6b7280';
  };

  const handleEventClick = (info: any) => {
    if (onEventClick) {
      onEventClick(info.event.extendedProps);
    }
  };

  const handleDateClick = (info: any) => {
    if (onDateClick) {
      onDateClick(info.date);
    }
  };

  return (
    <div className="training-calendar">
      <FullCalendar
        plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
        initialView="dayGridMonth"
        locale={esLocale}
        headerToolbar={{
          left: 'prev,next today',
          center: 'title',
          right: 'dayGridMonth,timeGridWeek,timeGridDay'
        }}
        events={events}
        eventClick={handleEventClick}
        dateClick={handleDateClick}
        height="auto"
        eventContent={(eventInfo) => (
          <div className="custom-event">
            <div className="event-title">{eventInfo.event.title}</div>
            <div className="event-details">
              {eventInfo.event.extendedProps.enrolled}/{eventInfo.event.extendedProps.capacity}
            </div>
          </div>
        )}
        eventDidMount={(info) => {
          info.el.setAttribute('title',
            `${info.event.title}\nInstructor: ${info.event.extendedProps.instructor}\nLugar: ${info.event.extendedProps.location}`
          );
        }}
      />
    </div>
  );
};

4.4.2 TrainingMatrix Component

// apps/verticales/construccion/frontend/src/modules/safety/features/trainings/components/TrainingMatrix.tsx

import React, { useEffect } from 'react';
import { Check, X, Clock, AlertCircle } from 'lucide-react';
import { useTrainingMatrix } from '../hooks/useTrainingMatrix';

interface TrainingMatrixProps {
  projectId: string;
}

export const TrainingMatrix: React.FC<TrainingMatrixProps> = ({ projectId }) => {
  const { matrix, fetchMatrix, isLoading } = useTrainingMatrix();

  useEffect(() => {
    fetchMatrix(projectId);
  }, [projectId]);

  const getCertificationStatus = (certification: any) => {
    if (!certification) return { icon: X, color: 'text-red-500', text: 'No cursado' };

    if (certification.expired) {
      return { icon: AlertCircle, color: 'text-orange-500', text: 'Vencido' };
    }

    if (certification.expiresSoon) {
      return { icon: Clock, color: 'text-yellow-500', text: 'Por vencer' };
    }

    return { icon: Check, color: 'text-green-500', text: 'Vigente' };
  };

  if (isLoading) return <div>Cargando matriz de capacitaciones...</div>;

  return (
    <div className="training-matrix">
      <div className="matrix-header">
        <h3>Matriz de Capacitaciones - {matrix?.projectName}</h3>
        <div className="matrix-stats">
          <span>Vigentes: {matrix?.stats.valid}</span>
          <span>Por vencer: {matrix?.stats.expiringSoon}</span>
          <span>Vencidos: {matrix?.stats.expired}</span>
          <span>Faltantes: {matrix?.stats.missing}</span>
        </div>
      </div>

      <div className="matrix-table-container">
        <table className="matrix-table">
          <thead>
            <tr>
              <th className="sticky-col">Empleado</th>
              <th className="sticky-col">Puesto</th>
              {matrix?.requiredTrainings.map(training => (
                <th key={training.id} className="training-header">
                  {training.name}
                </th>
              ))}
            </tr>
          </thead>
          <tbody>
            {matrix?.employees.map(employee => (
              <tr key={employee.id}>
                <td className="sticky-col employee-name">{employee.name}</td>
                <td className="sticky-col">{employee.role}</td>
                {matrix.requiredTrainings.map(training => {
                  const certification = employee.certifications.find(
                    c => c.trainingId === training.id
                  );
                  const status = getCertificationStatus(certification);
                  const Icon = status.icon;

                  return (
                    <td key={training.id} className="cert-cell">
                      <div className={`cert-status ${status.color}`}>
                        <Icon size={18} />
                        {certification && (
                          <span className="cert-date">
                            {new Date(certification.expirationDate).toLocaleDateString('es-ES')}
                          </span>
                        )}
                      </div>
                    </td>
                  );
                })}
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      <div className="matrix-legend">
        <h4>Leyenda:</h4>
        <div className="legend-items">
          <span><Check className="text-green-500" /> Vigente</span>
          <span><Clock className="text-yellow-500" /> Por vencer (30 días)</span>
          <span><AlertCircle className="text-orange-500" /> Vencido</span>
          <span><X className="text-red-500" /> No cursado</span>
        </div>
      </div>
    </div>
  );
};

4.4.3 Training Store

// apps/verticales/construccion/frontend/src/modules/safety/features/trainings/stores/trainingStore.ts

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

interface Training {
  id: string;
  title: string;
  description: string;
  category: string;
  duration: number;
  instructor: string;
  capacity: number;
  enrolled: number;
  startDate: string;
  endDate: string;
  location: string;
  status: 'scheduled' | 'in-progress' | 'completed' | 'cancelled';
  participants: string[];
  validityPeriod: number; // months
}

interface Certification {
  id: string;
  employeeId: string;
  trainingId: string;
  completionDate: string;
  expirationDate: string;
  score?: number;
  certificateUrl?: string;
}

interface TrainingStore {
  trainings: Training[];
  certifications: Certification[];
  selectedTraining: Training | null;
  isLoading: boolean;
  error: string | null;

  fetchTrainings: (filters?: any) => Promise<void>;
  getTrainingById: (id: string) => Promise<void>;
  createTraining: (data: any) => Promise<void>;
  updateTraining: (id: string, data: any) => Promise<void>;
  cancelTraining: (id: string, reason: string) => Promise<void>;
  enrollEmployee: (trainingId: string, employeeId: string) => Promise<void>;
  unenrollEmployee: (trainingId: string, employeeId: string) => Promise<void>;
  recordAttendance: (trainingId: string, attendees: string[]) => Promise<void>;
  issueCertification: (trainingId: string, employeeId: string, score?: number) => Promise<void>;
  fetchCertifications: (employeeId?: string) => Promise<void>;
  getExpiringCertifications: (days: number) => Certification[];
  setSelectedTraining: (training: Training | null) => void;
}

export const useTrainingStore = create<TrainingStore>()(
  devtools((set, get) => ({
    trainings: [],
    certifications: [],
    selectedTraining: null,
    isLoading: false,
    error: null,

    fetchTrainings: async (filters = {}) => {
      set({ isLoading: true, error: null });
      try {
        const queryParams = new URLSearchParams(filters);
        const response = await fetch(`/api/safety/trainings?${queryParams}`);
        const data = await response.json();
        set({ trainings: data, isLoading: false });
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    getTrainingById: async (id) => {
      set({ isLoading: true, error: null });
      try {
        const response = await fetch(`/api/safety/trainings/${id}`);
        const data = await response.json();
        set({ selectedTraining: data, isLoading: false });
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    createTraining: async (data) => {
      set({ isLoading: true, error: null });
      try {
        const response = await fetch('/api/safety/trainings', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });
        const newTraining = await response.json();
        set(state => ({
          trainings: [...state.trainings, newTraining],
          isLoading: false
        }));
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    updateTraining: async (id, data) => {
      set({ isLoading: true, error: null });
      try {
        const response = await fetch(`/api/safety/trainings/${id}`, {
          method: 'PUT',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(data)
        });
        const updated = await response.json();
        set(state => ({
          trainings: state.trainings.map(t => t.id === id ? updated : t),
          isLoading: false
        }));
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    cancelTraining: async (id, reason) => {
      set({ isLoading: true, error: null });
      try {
        await fetch(`/api/safety/trainings/${id}/cancel`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ reason })
        });
        await get().getTrainingById(id);
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    enrollEmployee: async (trainingId, employeeId) => {
      try {
        await fetch(`/api/safety/trainings/${trainingId}/enroll`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ employeeId })
        });
        await get().getTrainingById(trainingId);
      } catch (error) {
        console.error('Error enrolling employee:', error);
      }
    },

    unenrollEmployee: async (trainingId, employeeId) => {
      try {
        await fetch(`/api/safety/trainings/${trainingId}/unenroll`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ employeeId })
        });
        await get().getTrainingById(trainingId);
      } catch (error) {
        console.error('Error unenrolling employee:', error);
      }
    },

    recordAttendance: async (trainingId, attendees) => {
      try {
        await fetch(`/api/safety/trainings/${trainingId}/attendance`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ attendees })
        });
        await get().getTrainingById(trainingId);
      } catch (error) {
        console.error('Error recording attendance:', error);
      }
    },

    issueCertification: async (trainingId, employeeId, score) => {
      try {
        const response = await fetch(`/api/safety/trainings/${trainingId}/certify`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ employeeId, score })
        });
        const certification = await response.json();
        set(state => ({
          certifications: [...state.certifications, certification]
        }));
      } catch (error) {
        console.error('Error issuing certification:', error);
      }
    },

    fetchCertifications: async (employeeId) => {
      set({ isLoading: true, error: null });
      try {
        const url = employeeId
          ? `/api/safety/certifications?employeeId=${employeeId}`
          : '/api/safety/certifications';
        const response = await fetch(url);
        const data = await response.json();
        set({ certifications: data, isLoading: false });
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    getExpiringCertifications: (days) => {
      const threshold = new Date();
      threshold.setDate(threshold.getDate() + days);

      return get().certifications.filter(cert => {
        const expirationDate = new Date(cert.expirationDate);
        return expirationDate <= threshold && expirationDate > new Date();
      });
    },

    setSelectedTraining: (training) => set({ selectedTraining: training })
  }))
);

5. Safety Dashboard

5.1 SafetyDashboard Component

// apps/verticales/construccion/frontend/src/modules/safety/dashboard/components/SafetyDashboard.tsx

import React, { useEffect } from 'react';
import { IncidentRateKPI } from './IncidentRateKPI';
import { ComplianceKPI } from './ComplianceKPI';
import { EPPComplianceChart } from './EPPComplianceChart';
import { TrainingCompletionChart } from './TrainingCompletionChart';
import { IncidentTrendChart } from './IncidentTrendChart';
import { SafetyAlerts } from './SafetyAlerts';
import { useSafetyMetrics } from '../hooks/useSafetyMetrics';

interface SafetyDashboardProps {
  projectId?: string;
  dateRange?: { start: Date; end: Date };
}

export const SafetyDashboard: React.FC<SafetyDashboardProps> = ({
  projectId,
  dateRange
}) => {
  const { metrics, fetchMetrics, isLoading } = useSafetyMetrics();

  useEffect(() => {
    fetchMetrics({ projectId, dateRange });
  }, [projectId, dateRange]);

  if (isLoading) {
    return <div className="dashboard-loading">Cargando métricas de seguridad...</div>;
  }

  return (
    <div className="safety-dashboard">
      <div className="dashboard-header">
        <h1>Dashboard de Seguridad Industrial</h1>
        <div className="dashboard-filters">
          {/* Filters for project and date range */}
        </div>
      </div>

      <div className="kpi-grid">
        <IncidentRateKPI
          rate={metrics?.incidentRate || 0}
          trend={metrics?.incidentTrend || 0}
          previousPeriod={metrics?.previousIncidentRate || 0}
        />
        <ComplianceKPI
          percentage={metrics?.compliancePercentage || 0}
          trend={metrics?.complianceTrend || 0}
          inspections={metrics?.totalInspections || 0}
        />
        <div className="kpi-card">
          <h3>EPP Asignados</h3>
          <div className="kpi-value">{metrics?.eppAssigned || 0}</div>
          <div className="kpi-subtitle">
            {metrics?.eppExpiringSoon || 0} por vencer
          </div>
        </div>
        <div className="kpi-card">
          <h3>Capacitaciones</h3>
          <div className="kpi-value">{metrics?.trainingsCompleted || 0}</div>
          <div className="kpi-subtitle">
            {metrics?.trainingsPending || 0} pendientes
          </div>
        </div>
      </div>

      <div className="dashboard-charts">
        <div className="chart-row">
          <div className="chart-container">
            <IncidentTrendChart
              data={metrics?.incidentTrendData || []}
            />
          </div>
          <div className="chart-container">
            <EPPComplianceChart
              data={metrics?.eppComplianceData || []}
            />
          </div>
        </div>

        <div className="chart-row">
          <div className="chart-container">
            <TrainingCompletionChart
              data={metrics?.trainingCompletionData || []}
            />
          </div>
          <div className="chart-container">
            <SafetyAlerts
              alerts={metrics?.alerts || []}
            />
          </div>
        </div>
      </div>
    </div>
  );
};

5.2 IncidentRateKPI Component

// apps/verticales/construccion/frontend/src/modules/safety/dashboard/components/IncidentRateKPI.tsx

import React from 'react';
import { TrendingDown, TrendingUp, Minus } from 'lucide-react';

interface IncidentRateKPIProps {
  rate: number;
  trend: number;
  previousPeriod: number;
}

export const IncidentRateKPI: React.FC<IncidentRateKPIProps> = ({
  rate,
  trend,
  previousPeriod
}) => {
  const getTrendIcon = () => {
    if (trend < 0) return <TrendingDown className="text-green-500" />;
    if (trend > 0) return <TrendingUp className="text-red-500" />;
    return <Minus className="text-gray-500" />;
  };

  const getTrendColor = () => {
    if (trend < 0) return 'text-green-500';
    if (trend > 0) return 'text-red-500';
    return 'text-gray-500';
  };

  return (
    <div className="kpi-card incident-rate">
      <div className="kpi-header">
        <h3>Tasa de Incidentes</h3>
        <div className={`kpi-trend ${getTrendColor()}`}>
          {getTrendIcon()}
          <span>{Math.abs(trend).toFixed(1)}%</span>
        </div>
      </div>

      <div className="kpi-value">
        {rate.toFixed(2)}
      </div>

      <div className="kpi-subtitle">
        por cada 100 trabajadores
      </div>

      <div className="kpi-comparison">
        Período anterior: {previousPeriod.toFixed(2)}
      </div>
    </div>
  );
};

5.3 ComplianceKPI Component

// apps/verticales/construccion/frontend/src/modules/safety/dashboard/components/ComplianceKPI.tsx

import React from 'react';
import { CheckCircle } from 'lucide-react';

interface ComplianceKPIProps {
  percentage: number;
  trend: number;
  inspections: number;
}

export const ComplianceKPI: React.FC<ComplianceKPIProps> = ({
  percentage,
  trend,
  inspections
}) => {
  const getComplianceLevel = () => {
    if (percentage >= 90) return { level: 'Excelente', color: 'text-green-500' };
    if (percentage >= 75) return { level: 'Bueno', color: 'text-blue-500' };
    if (percentage >= 60) return { level: 'Aceptable', color: 'text-yellow-500' };
    return { level: 'Crítico', color: 'text-red-500' };
  };

  const complianceLevel = getComplianceLevel();

  return (
    <div className="kpi-card compliance">
      <div className="kpi-header">
        <h3>Cumplimiento de Seguridad</h3>
        <CheckCircle className={complianceLevel.color} size={24} />
      </div>

      <div className={`kpi-value ${complianceLevel.color}`}>
        {percentage.toFixed(1)}%
      </div>

      <div className="kpi-subtitle">
        {complianceLevel.level}
      </div>

      <div className="kpi-comparison">
        {inspections} inspecciones realizadas
      </div>

      <div className="compliance-bar">
        <div
          className="compliance-fill"
          style={{
            width: `${percentage}%`,
            backgroundColor: complianceLevel.color.replace('text-', '')
          }}
        />
      </div>
    </div>
  );
};

5.4 Dashboard Store

// apps/verticales/construccion/frontend/src/modules/safety/dashboard/stores/dashboardStore.ts

import { create } from 'zustand';
import { devtools } from 'zustand/middleware';

interface SafetyMetrics {
  incidentRate: number;
  incidentTrend: number;
  previousIncidentRate: number;
  compliancePercentage: number;
  complianceTrend: number;
  totalInspections: number;
  eppAssigned: number;
  eppExpiringSoon: number;
  trainingsCompleted: number;
  trainingsPending: number;
  incidentTrendData: Array<{ date: string; count: number }>;
  eppComplianceData: Array<{ category: string; compliance: number }>;
  trainingCompletionData: Array<{ month: string; completed: number; total: number }>;
  alerts: Array<{
    id: string;
    type: 'critical' | 'warning' | 'info';
    message: string;
    timestamp: string;
  }>;
}

interface DashboardStore {
  metrics: SafetyMetrics | null;
  filters: {
    projectId?: string;
    dateRange?: { start: Date; end: Date };
  };
  isLoading: boolean;
  error: string | null;

  fetchMetrics: (filters?: any) => Promise<void>;
  setFilters: (filters: any) => void;
  refreshMetrics: () => Promise<void>;
}

export const useDashboardStore = create<DashboardStore>()(
  devtools((set, get) => ({
    metrics: null,
    filters: {},
    isLoading: false,
    error: null,

    fetchMetrics: async (filters = {}) => {
      set({ isLoading: true, error: null, filters });
      try {
        const queryParams = new URLSearchParams({
          projectId: filters.projectId || '',
          startDate: filters.dateRange?.start?.toISOString() || '',
          endDate: filters.dateRange?.end?.toISOString() || ''
        });

        const response = await fetch(`/api/safety/dashboard/metrics?${queryParams}`);
        const data = await response.json();
        set({ metrics: data, isLoading: false });
      } catch (error) {
        set({ error: error.message, isLoading: false });
      }
    },

    setFilters: (filters) => {
      set({ filters });
      get().fetchMetrics(filters);
    },

    refreshMetrics: async () => {
      const currentFilters = get().filters;
      await get().fetchMetrics(currentFilters);
    }
  }))
);

6. Routing

// apps/verticales/construccion/frontend/src/modules/safety/routes/safetyRoutes.tsx

import { Routes, Route } from 'react-router-dom';
import { SafetyDashboard } from '../dashboard/components/SafetyDashboard';
import { EPPAssignmentForm } from '../features/epp/components/EPPAssignmentForm';
import { EPPInventoryList } from '../features/epp/components/EPPInventoryList';
import { SafetyChecklist } from '../features/safety-inspections/components/SafetyChecklist';
import { InspectionHistory } from '../features/safety-inspections/components/InspectionHistory';
import { IncidentReportForm } from '../features/incidents/components/IncidentReportForm';
import { IncidentDetails } from '../features/incidents/components/IncidentDetails';
import { TrainingCalendar } from '../features/trainings/components/TrainingCalendar';
import { TrainingMatrix } from '../features/trainings/components/TrainingMatrix';

export const SafetyRoutes = () => {
  return (
    <Routes>
      <Route path="/" element={<SafetyDashboard />} />

      {/* EPP Routes */}
      <Route path="/epp">
        <Route index element={<EPPInventoryList />} />
        <Route path="assign" element={<EPPAssignmentForm />} />
      </Route>

      {/* Inspection Routes */}
      <Route path="/inspections">
        <Route index element={<InspectionHistory />} />
        <Route path="new" element={<SafetyChecklist />} />
      </Route>

      {/* Incident Routes */}
      <Route path="/incidents">
        <Route index element={<div>Incident List</div>} />
        <Route path="new" element={<IncidentReportForm />} />
        <Route path=":id" element={<IncidentDetails />} />
      </Route>

      {/* Training Routes */}
      <Route path="/trainings">
        <Route index element={<TrainingCalendar />} />
        <Route path="matrix" element={<TrainingMatrix />} />
      </Route>
    </Routes>
  );
};

7. Types & Interfaces

// apps/verticales/construccion/frontend/src/modules/safety/shared/types/index.ts

export interface Employee {
  id: string;
  name: string;
  role: string;
  projectId: string;
}

export interface Project {
  id: string;
  name: string;
  code: string;
}

export type IncidentType = 'accident' | 'near-miss' | 'property-damage' | 'environmental';
export type IncidentSeverity = 'minor' | 'moderate' | 'serious' | 'critical' | 'fatal';
export type IncidentStatus = 'reported' | 'investigating' | 'actions-pending' | 'closed';

export type InspectionStatus = 'draft' | 'completed' | 'requires-followup';
export type ChecklistItemStatus = 'compliant' | 'non-compliant' | 'not-applicable';

export type TrainingStatus = 'scheduled' | 'in-progress' | 'completed' | 'cancelled';
export type EPPStatus = 'active' | 'returned' | 'expired';

8. Configuración de Vite

// apps/verticales/construccion/frontend/vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@safety': path.resolve(__dirname, './src/modules/safety')
    }
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:4000',
        changeOrigin: true
      }
    }
  },
  build: {
    outDir: 'dist',
    sourcemap: true
  }
});

9. Package.json

{
  "name": "@erp-suite/construccion-safety-frontend",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview",
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-router-dom": "^6.22.0",
    "zustand": "^4.5.0",
    "react-hook-form": "^7.51.0",
    "@hookform/resolvers": "^3.3.4",
    "zod": "^3.22.0",
    "@fullcalendar/react": "^6.1.0",
    "@fullcalendar/daygrid": "^6.1.0",
    "@fullcalendar/timegrid": "^6.1.0",
    "@fullcalendar/interaction": "^6.1.0",
    "date-fns": "^3.3.0",
    "recharts": "^2.12.0",
    "lucide-react": "^0.344.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "@typescript-eslint/eslint-plugin": "^6.21.0",
    "@typescript-eslint/parser": "^6.21.0",
    "@vitejs/plugin-react": "^4.2.1",
    "eslint": "^8.56.0",
    "eslint-plugin-react-hooks": "^4.6.0",
    "eslint-plugin-react-refresh": "^0.4.5",
    "typescript": "^5.3.3",
    "vite": "^5.1.0"
  }
}

10. Consideraciones de Implementación

10.1 Estado Global vs Local

  • Zustand (Global): Para datos compartidos entre múltiples componentes (inventario EPP, lista de incidentes, capacitaciones)
  • useState (Local): Para estado UI específico de componentes (formularios, modales, filtros temporales)

10.2 Performance

  • Implementar virtualización para listas largas (matriz de capacitaciones)
  • Memoización de cálculos complejos (tasas de incidentes, cumplimiento)
  • Lazy loading de imágenes en reportes de incidentes
  • Code splitting por feature module

10.3 Validaciones

  • Validación en tiempo real con react-hook-form + zod
  • Validaciones de negocio en el frontend antes de enviar al backend
  • Mensajes de error descriptivos y contextuales

10.4 Accesibilidad

  • Uso de semantic HTML
  • Labels apropiados en formularios
  • Soporte de navegación por teclado
  • Contraste de colores adecuado en KPIs

10.5 Testing (Futuro)

  • Unit tests con Vitest
  • Component tests con React Testing Library
  • E2E tests con Playwright

11. Próximos Pasos

  1. Implementar autenticación y autorización
  2. Agregar soporte offline con PWA
  3. Implementar notificaciones push para alertas críticas
  4. Agregar exportación de reportes a PDF/Excel
  5. Implementar sistema de firma digital para documentos
  6. Agregar soporte multiidioma (i18n)