workspace-v1/projects/erp-construccion/docs/02-definicion-modulos/MAI-007-seguridad-industrial/especificaciones/ET-SEG-001-backend.md
rckrdmrd 66161b1566 feat: Workspace-v1 complete migration with NEXUS v3.4
Sistema NEXUS v3.4 migrado con:

Estructura principal:
- core/orchestration: Sistema SIMCO + CAPVED (27 directivas, 28 perfiles)
- core/catalog: Catalogo de funcionalidades reutilizables
- shared/knowledge-base: Base de conocimiento compartida
- devtools/scripts: Herramientas de desarrollo
- control-plane/registries: Control de servicios y CI/CD
- orchestration/: Configuracion de orquestacion de agentes

Proyectos incluidos (11):
- gamilit (submodule -> GitHub)
- trading-platform (OrbiquanTIA)
- erp-suite con 5 verticales:
  - erp-core, construccion, vidrio-templado
  - mecanicas-diesel, retail, clinicas
- betting-analytics
- inmobiliaria-analytics
- platform_marketing_content
- pos-micro, erp-basico

Configuracion:
- .gitignore completo para Node.js/Python/Docker
- gamilit como submodule (git@github.com:rckrdmrd/gamilit-workspace.git)
- Sistema de puertos estandarizado (3005-3199)

Generated with NEXUS v3.4 Migration System
EPIC-010: Configuracion Git y Repositorios
2026-01-04 03:37:42 -06:00

79 KiB

ET-SEG-001: Especificaciones Técnicas Backend - Seguridad Industrial

1. Información General

Módulo: MAI-007 - Seguridad Industrial Componente: Backend API Stack Tecnológico: NestJS 10+, TypeORM Responsable: Equipo Backend Fecha: 2025-12-06 Versión: 1.0.0

2. Arquitectura de Módulos

2.1. Estructura de Módulos NestJS

src/
├── modules/
   ├── epp/                          # Equipos de Protección Personal
      ├── epp.module.ts
      ├── entities/
      ├── dto/
      ├── services/
      └── controllers/
   ├── safety-inspections/           # Inspecciones de Seguridad
      ├── safety-inspections.module.ts
      ├── entities/
      ├── dto/
      ├── services/
      └── controllers/
   ├── incidents/                    # Gestión de Incidentes
      ├── incidents.module.ts
      ├── entities/
      ├── dto/
      ├── services/
      ├── controllers/
      └── workflows/
   └── safety-trainings/             # Capacitaciones de Seguridad
       ├── safety-trainings.module.ts
       ├── entities/
       ├── dto/
       ├── services/
       └── controllers/
├── shared/
   ├── notifications/
   └── reporting/
└── config/

2.2. EPPModule - Equipos de Protección Personal

// epp.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
  EPPItem,
  EPPAssignment,
  EPPInventory,
  EPPDelivery,
  EPPCategory
} from './entities';
import { EPPService } from './services/epp.service';
import { EPPAssignmentService } from './services/epp-assignment.service';
import { EPPInventoryService } from './services/epp-inventory.service';
import { EPPController } from './controllers/epp.controller';

@Module({
  imports: [
    TypeOrmModule.forFeature([
      EPPItem,
      EPPAssignment,
      EPPInventory,
      EPPDelivery,
      EPPCategory
    ])
  ],
  controllers: [EPPController],
  providers: [
    EPPService,
    EPPAssignmentService,
    EPPInventoryService
  ],
  exports: [EPPService, EPPAssignmentService]
})
export class EPPModule {}

2.3. SafetyInspectionsModule - Inspecciones de Seguridad

// safety-inspections.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
  SafetyInspection,
  InspectionTemplate,
  InspectionItem,
  InspectionResponse,
  InspectionEvidence
} from './entities';
import { SafetyInspectionsService } from './services/safety-inspections.service';
import { InspectionTemplatesService } from './services/inspection-templates.service';
import { SafetyInspectionsController } from './controllers/safety-inspections.controller';

@Module({
  imports: [
    TypeOrmModule.forFeature([
      SafetyInspection,
      InspectionTemplate,
      InspectionItem,
      InspectionResponse,
      InspectionEvidence
    ])
  ],
  controllers: [SafetyInspectionsController],
  providers: [
    SafetyInspectionsService,
    InspectionTemplatesService
  ],
  exports: [SafetyInspectionsService]
})
export class SafetyInspectionsModule {}

2.4. IncidentsModule - Gestión de Incidentes

// incidents.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
  Incident,
  IncidentInvestigation,
  IncidentWitness,
  IncidentEvidence,
  CorrectiveAction,
  RootCauseAnalysis
} from './entities';
import { IncidentsService } from './services/incidents.service';
import { IncidentInvestigationService } from './services/incident-investigation.service';
import { CorrectiveActionsService } from './services/corrective-actions.service';
import { IncidentWorkflowService } from './workflows/incident-workflow.service';
import { IncidentsController } from './controllers/incidents.controller';

@Module({
  imports: [
    TypeOrmModule.forFeature([
      Incident,
      IncidentInvestigation,
      IncidentWitness,
      IncidentEvidence,
      CorrectiveAction,
      RootCauseAnalysis
    ])
  ],
  controllers: [IncidentsController],
  providers: [
    IncidentsService,
    IncidentInvestigationService,
    CorrectiveActionsService,
    IncidentWorkflowService
  ],
  exports: [IncidentsService, IncidentWorkflowService]
})
export class IncidentsModule {}

2.5. SafetyTrainingsModule - Capacitaciones

// safety-trainings.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
  SafetyTraining,
  TrainingCourse,
  TrainingAttendance,
  TrainingCertification,
  TrainingRequirement
} from './entities';
import { SafetyTrainingsService } from './services/safety-trainings.service';
import { TrainingCertificationsService } from './services/training-certifications.service';
import { SafetyTrainingsController } from './controllers/safety-trainings.controller';

@Module({
  imports: [
    TypeOrmModule.forFeature([
      SafetyTraining,
      TrainingCourse,
      TrainingAttendance,
      TrainingCertification,
      TrainingRequirement
    ])
  ],
  controllers: [SafetyTrainingsController],
  providers: [
    SafetyTrainingsService,
    TrainingCertificationsService
  ],
  exports: [SafetyTrainingsService]
})
export class SafetyTrainingsModule {}

3. Entities y Modelos de Datos

3.1. EPP - Equipos de Protección Personal

// entities/epp-item.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('epp_items')
export class EPPItem {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  code: string;

  @Column()
  name: string;

  @Column('text', { nullable: true })
  description: string;

  @ManyToOne(() => EPPCategory)
  category: EPPCategory;

  @Column('uuid')
  categoryId: string;

  @Column()
  brand: string;

  @Column({ nullable: true })
  model: string;

  @Column('jsonb', { nullable: true })
  specifications: {
    size?: string;
    color?: string;
    material?: string;
    certifications?: string[];
  };

  @Column('decimal', { precision: 10, scale: 2 })
  unitCost: number;

  @Column('int')
  usefulLifeDays: number; // Vida útil en días

  @Column('boolean', { default: true })
  requiresCertification: boolean;

  @Column('jsonb', { nullable: true })
  nomCompliance: {
    norm: string;
    requirement: string;
    validUntil?: Date;
  }[];

  @Column('boolean', { default: true })
  isActive: true;

  @OneToMany(() => EPPInventory, inventory => inventory.eppItem)
  inventory: EPPInventory[];

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

// entities/epp-category.entity.ts
@Entity('epp_categories')
export class EPPCategory {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  name: string;

  @Column('text', { nullable: true })
  description: string;

  @Column({ nullable: true })
  icon: string;

  @Column('simple-array', { nullable: true })
  requiredByRoles: string[]; // Roles que requieren este EPP

  @Column('boolean', { default: true })
  isMandatory: boolean;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

// entities/epp-assignment.entity.ts
@Entity('epp_assignments')
export class EPPAssignment {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column('uuid')
  workerId: string;

  @ManyToOne(() => EPPItem)
  eppItem: EPPItem;

  @Column('uuid')
  eppItemId: string;

  @Column('uuid')
  projectId: string;

  @Column('date')
  assignmentDate: Date;

  @Column({ nullable: true })
  serialNumber: string; // Para EPP individualizado

  @Column()
  quantity: number;

  @Column('date')
  expectedReturnDate: Date;

  @Column('date', { nullable: true })
  actualReturnDate: Date;

  @Column({
    type: 'enum',
    enum: ['assigned', 'in_use', 'returned', 'damaged', 'lost'],
    default: 'assigned'
  })
  status: string;

  @Column('text', { nullable: true })
  condition: string;

  @Column('jsonb', { nullable: true })
  maintenanceHistory: {
    date: Date;
    type: string;
    notes: string;
  }[];

  @Column('uuid')
  assignedBy: string;

  @Column('uuid', { nullable: true })
  receivedBy: string;

  @Column('text', { nullable: true })
  notes: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

// entities/epp-inventory.entity.ts
@Entity('epp_inventory')
export class EPPInventory {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @ManyToOne(() => EPPItem, item => item.inventory)
  eppItem: EPPItem;

  @Column('uuid')
  eppItemId: string;

  @Column('uuid')
  warehouseId: string;

  @Column('uuid', { nullable: true })
  projectId: string; // null = almacén central

  @Column('int')
  quantity: number;

  @Column('int')
  minimumStock: number;

  @Column('int')
  maximumStock: number;

  @Column('int', { default: 0 })
  reserved: number; // Cantidad reservada pero no asignada

  @Column('date', { nullable: true })
  lastRestockDate: Date;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

// entities/epp-delivery.entity.ts
@Entity('epp_deliveries')
export class EPPDelivery {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  deliveryNumber: string;

  @Column('uuid')
  workerId: string;

  @Column('uuid')
  projectId: string;

  @Column('jsonb')
  items: {
    eppItemId: string;
    quantity: number;
    serialNumbers?: string[];
  }[];

  @Column('date')
  deliveryDate: Date;

  @Column('uuid')
  deliveredBy: string;

  @Column({ nullable: true })
  signatureUrl: string; // Firma digital del trabajador

  @Column('text', { nullable: true })
  notes: string;

  @CreateDateColumn()
  createdAt: Date;
}

3.2. Safety Inspections - Inspecciones

// entities/inspection-template.entity.ts
@Entity('inspection_templates')
export class InspectionTemplate {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  code: string;

  @Column()
  name: string;

  @Column('text', { nullable: true })
  description: string;

  @Column({
    type: 'enum',
    enum: ['epp', 'workplace', 'equipment', 'vehicle', 'scaffolding', 'electrical', 'general']
  })
  inspectionType: string;

  @Column('jsonb')
  sections: {
    id: string;
    name: string;
    order: number;
    items: {
      id: string;
      question: string;
      type: 'yes_no' | 'scale' | 'text' | 'numeric' | 'checklist';
      required: boolean;
      options?: string[];
      criticalItem?: boolean; // Si falla, requiere acción inmediata
    }[];
  }[];

  @Column('int')
  frequency: number; // Frecuencia en días

  @Column('simple-array', { nullable: true })
  applicableRoles: string[];

  @Column('boolean', { default: true })
  requiresEvidence: boolean;

  @Column('boolean', { default: true })
  isActive: boolean;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

// entities/safety-inspection.entity.ts
@Entity('safety_inspections')
export class SafetyInspection {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  inspectionNumber: string;

  @ManyToOne(() => InspectionTemplate)
  template: InspectionTemplate;

  @Column('uuid')
  templateId: string;

  @Column('uuid')
  projectId: string;

  @Column('uuid', { nullable: true })
  inspectedAreaId: string; // Área/zona inspeccionada

  @Column('uuid')
  inspectorId: string;

  @Column('timestamp')
  inspectionDate: Date;

  @Column('timestamp', { nullable: true })
  scheduledDate: Date;

  @Column({
    type: 'enum',
    enum: ['scheduled', 'in_progress', 'completed', 'cancelled'],
    default: 'scheduled'
  })
  status: string;

  @OneToMany(() => InspectionResponse, response => response.inspection, { cascade: true })
  responses: InspectionResponse[];

  @Column('decimal', { precision: 5, scale: 2, nullable: true })
  overallScore: number; // Puntuación general

  @Column('int', { default: 0 })
  criticalFindings: number;

  @Column('int', { default: 0 })
  majorFindings: number;

  @Column('int', { default: 0 })
  minorFindings: number;

  @Column('jsonb', { nullable: true })
  geolocation: {
    latitude: number;
    longitude: number;
    accuracy: number;
  };

  @Column('text', { nullable: true })
  generalObservations: string;

  @Column('simple-array', { nullable: true })
  attachments: string[]; // URLs de fotos/documentos

  @Column({ nullable: true })
  signature: string; // Firma digital del inspector

  @Column('timestamp', { nullable: true })
  submittedAt: Date;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

// entities/inspection-response.entity.ts
@Entity('inspection_responses')
export class InspectionResponse {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @ManyToOne(() => SafetyInspection, inspection => inspection.responses)
  inspection: SafetyInspection;

  @Column('uuid')
  inspectionId: string;

  @Column()
  sectionId: string;

  @Column()
  itemId: string;

  @Column('text')
  response: string; // Respuesta según el tipo de pregunta

  @Column('boolean', { default: false })
  isNonCompliant: boolean;

  @Column({
    type: 'enum',
    enum: ['critical', 'major', 'minor', 'observation'],
    nullable: true
  })
  severity: string;

  @Column('text', { nullable: true })
  notes: string;

  @Column('simple-array', { nullable: true })
  evidenceUrls: string[];

  @Column('boolean', { default: false })
  requiresCorrectiveAction: boolean;

  @CreateDateColumn()
  createdAt: Date;
}

3.3. Incidents - Gestión de Incidentes

// entities/incident.entity.ts
@Entity('incidents')
export class Incident {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  incidentNumber: string;

  @Column('uuid')
  projectId: string;

  @Column({
    type: 'enum',
    enum: ['accident', 'near_miss', 'unsafe_condition', 'property_damage', 'environmental']
  })
  type: string;

  @Column({
    type: 'enum',
    enum: ['fatal', 'serious', 'moderate', 'minor', 'first_aid']
  })
  severity: string;

  @Column('timestamp')
  incidentDate: Date;

  @Column('time')
  incidentTime: string;

  @Column()
  location: string;

  @Column('uuid', { nullable: true })
  areaId: string;

  @Column('text')
  description: string;

  @Column('text', { nullable: true })
  immediateActions: string;

  @Column('jsonb', { nullable: true })
  affectedPersons: {
    workerId: string;
    name: string;
    role: string;
    injuryType?: string;
    injuryLocation?: string;
    hospitalizedRequired?: boolean;
  }[];

  @Column('uuid')
  reportedBy: string;

  @Column('timestamp')
  reportedAt: Date;

  @Column({
    type: 'enum',
    enum: ['reported', 'investigating', 'analysis', 'corrective_actions', 'closed'],
    default: 'reported'
  })
  status: string;

  @Column('int', { default: 0 })
  workDaysLost: number;

  @Column('boolean', { default: false })
  requiresAuthority: boolean; // Requiere notificación a autoridades

  @Column('boolean', { default: false })
  notifiedToSTPS: boolean; // Notificado a STPS

  @Column('timestamp', { nullable: true })
  stpsNotificationDate: Date;

  @Column('simple-array', { nullable: true })
  evidenceUrls: string[];

  @Column('jsonb', { nullable: true })
  geolocation: {
    latitude: number;
    longitude: number;
  };

  @OneToMany(() => IncidentInvestigation, investigation => investigation.incident)
  investigations: IncidentInvestigation[];

  @OneToMany(() => IncidentWitness, witness => witness.incident)
  witnesses: IncidentWitness[];

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

// entities/incident-investigation.entity.ts
@Entity('incident_investigations')
export class IncidentInvestigation {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @ManyToOne(() => Incident, incident => incident.investigations)
  incident: Incident;

  @Column('uuid')
  incidentId: string;

  @Column('uuid')
  investigatorId: string;

  @Column('date')
  investigationDate: Date;

  @Column({
    type: 'enum',
    enum: ['initiated', 'in_progress', 'completed'],
    default: 'initiated'
  })
  status: string;

  @Column('text', { nullable: true })
  findings: string;

  @Column('text', { nullable: true })
  contributingFactors: string;

  @Column('jsonb', { nullable: true })
  rootCauseAnalysis: {
    method: '5why' | 'fishbone' | 'fta'; // Five Whys, Fishbone, Fault Tree Analysis
    analysis: any;
    rootCauses: string[];
  };

  @Column('text', { nullable: true })
  recommendations: string;

  @Column('simple-array', { nullable: true })
  investigationDocuments: string[];

  @OneToMany(() => CorrectiveAction, action => action.investigation)
  correctiveActions: CorrectiveAction[];

  @Column('timestamp', { nullable: true })
  completedAt: Date;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

// entities/incident-witness.entity.ts
@Entity('incident_witnesses')
export class IncidentWitness {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @ManyToOne(() => Incident, incident => incident.witnesses)
  incident: Incident;

  @Column('uuid')
  incidentId: string;

  @Column('uuid', { nullable: true })
  workerId: string; // null si es testigo externo

  @Column()
  name: string;

  @Column()
  role: string;

  @Column({ nullable: true })
  company: string; // Para subcontratistas

  @Column('text')
  statement: string;

  @Column('timestamp')
  statementDate: Date;

  @Column({ nullable: true })
  signatureUrl: string;

  @Column()
  contactPhone: string;

  @CreateDateColumn()
  createdAt: Date;
}

// entities/corrective-action.entity.ts
@Entity('corrective_actions')
export class CorrectiveAction {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @ManyToOne(() => IncidentInvestigation, investigation => investigation.correctiveActions)
  investigation: IncidentInvestigation;

  @Column('uuid')
  investigationId: string;

  @Column()
  actionNumber: string;

  @Column('text')
  description: string;

  @Column({
    type: 'enum',
    enum: ['immediate', 'corrective', 'preventive']
  })
  actionType: string;

  @Column({
    type: 'enum',
    enum: ['critical', 'high', 'medium', 'low']
  })
  priority: string;

  @Column('uuid')
  responsibleId: string;

  @Column('date')
  dueDate: Date;

  @Column({
    type: 'enum',
    enum: ['pending', 'in_progress', 'completed', 'verified', 'overdue'],
    default: 'pending'
  })
  status: string;

  @Column('text', { nullable: true })
  implementationNotes: string;

  @Column('date', { nullable: true })
  completionDate: Date;

  @Column('uuid', { nullable: true })
  verifiedBy: string;

  @Column('date', { nullable: true })
  verificationDate: Date;

  @Column('text', { nullable: true })
  verificationNotes: string;

  @Column('simple-array', { nullable: true })
  evidenceUrls: string[];

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

3.4. Safety Trainings - Capacitaciones

// entities/training-course.entity.ts
@Entity('training_courses')
export class TrainingCourse {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  code: string;

  @Column()
  name: string;

  @Column('text')
  description: string;

  @Column({
    type: 'enum',
    enum: ['safety_induction', 'epp_usage', 'emergency_response', 'first_aid', 'heights', 'confined_spaces', 'hazmat', 'electrical', 'machinery', 'nom_compliance']
  })
  category: string;

  @Column('int')
  durationHours: number;

  @Column('boolean', { default: false })
  isMandatory: boolean;

  @Column('simple-array', { nullable: true })
  requiredByRoles: string[];

  @Column('int', { nullable: true })
  validityMonths: number; // Meses de validez del certificado

  @Column('int', { nullable: true })
  passingScore: number; // Calificación mínima aprobatoria

  @Column('jsonb', { nullable: true })
  curriculum: {
    topics: string[];
    objectives: string[];
    evaluation: string;
  };

  @Column('jsonb', { nullable: true })
  nomCompliance: {
    norm: string;
    requirement: string;
  }[];

  @Column({ nullable: true })
  certificateTemplateId: string;

  @Column('boolean', { default: true })
  isActive: boolean;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

// entities/safety-training.entity.ts
@Entity('safety_trainings')
export class SafetyTraining {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  trainingNumber: string;

  @ManyToOne(() => TrainingCourse)
  course: TrainingCourse;

  @Column('uuid')
  courseId: string;

  @Column('uuid', { nullable: true })
  projectId: string; // null = corporativo

  @Column('date')
  trainingDate: Date;

  @Column('time')
  startTime: string;

  @Column('time')
  endTime: string;

  @Column()
  location: string;

  @Column('uuid')
  instructorId: string;

  @Column({ nullable: true })
  externalInstructor: string; // Para instructores externos

  @Column('int')
  capacity: number;

  @Column({
    type: 'enum',
    enum: ['scheduled', 'in_progress', 'completed', 'cancelled'],
    default: 'scheduled'
  })
  status: string;

  @OneToMany(() => TrainingAttendance, attendance => attendance.training)
  attendances: TrainingAttendance[];

  @Column('text', { nullable: true })
  materials: string; // Material didáctico utilizado

  @Column('simple-array', { nullable: true })
  attachments: string[];

  @Column('text', { nullable: true })
  notes: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

// entities/training-attendance.entity.ts
@Entity('training_attendances')
export class TrainingAttendance {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @ManyToOne(() => SafetyTraining, training => training.attendances)
  training: SafetyTraining;

  @Column('uuid')
  trainingId: string;

  @Column('uuid')
  workerId: string;

  @Column('boolean', { default: false })
  attended: boolean;

  @Column('int', { nullable: true })
  score: number; // Calificación de evaluación

  @Column('boolean', { default: false })
  passed: boolean;

  @Column({ nullable: true })
  signatureUrl: string;

  @Column('timestamp', { nullable: true })
  signedAt: Date;

  @Column('text', { nullable: true })
  notes: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

// entities/training-certification.entity.ts
@Entity('training_certifications')
export class TrainingCertification {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  certificateNumber: string;

  @Column('uuid')
  workerId: string;

  @ManyToOne(() => TrainingCourse)
  course: TrainingCourse;

  @Column('uuid')
  courseId: string;

  @ManyToOne(() => SafetyTraining)
  training: SafetyTraining;

  @Column('uuid')
  trainingId: string;

  @Column('date')
  issueDate: Date;

  @Column('date')
  expirationDate: Date;

  @Column({
    type: 'enum',
    enum: ['active', 'expired', 'revoked'],
    default: 'active'
  })
  status: string;

  @Column({ nullable: true })
  certificateUrl: string; // PDF del certificado

  @Column('uuid')
  issuedBy: string;

  @Column('text', { nullable: true })
  notes: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

4. DTOs (Data Transfer Objects)

4.1. EPP DTOs

// dto/create-epp-assignment.dto.ts
import { IsUUID, IsDate, IsNumber, IsString, IsOptional, IsEnum, Min } from 'class-validator';
import { Type } from 'class-transformer';

export class CreateEPPAssignmentDto {
  @IsUUID()
  workerId: string;

  @IsUUID()
  eppItemId: string;

  @IsUUID()
  projectId: string;

  @Type(() => Date)
  @IsDate()
  assignmentDate: Date;

  @IsNumber()
  @Min(1)
  quantity: number;

  @IsOptional()
  @IsString()
  serialNumber?: string;

  @Type(() => Date)
  @IsDate()
  expectedReturnDate: Date;

  @IsOptional()
  @IsString()
  notes?: string;
}

// dto/epp-delivery.dto.ts
export class CreateEPPDeliveryDto {
  @IsUUID()
  workerId: string;

  @IsUUID()
  projectId: string;

  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => EPPDeliveryItemDto)
  items: EPPDeliveryItemDto[];

  @Type(() => Date)
  @IsDate()
  deliveryDate: Date;

  @IsOptional()
  @IsString()
  signatureUrl?: string;

  @IsOptional()
  @IsString()
  notes?: string;
}

class EPPDeliveryItemDto {
  @IsUUID()
  eppItemId: string;

  @IsNumber()
  @Min(1)
  quantity: number;

  @IsOptional()
  @IsArray()
  @IsString({ each: true })
  serialNumbers?: string[];
}

4.2. Inspections DTOs

// dto/create-inspection.dto.ts
export class CreateSafetyInspectionDto {
  @IsUUID()
  templateId: string;

  @IsUUID()
  projectId: string;

  @IsOptional()
  @IsUUID()
  inspectedAreaId?: string;

  @Type(() => Date)
  @IsDate()
  inspectionDate: Date;

  @IsOptional()
  @Type(() => Date)
  @IsDate()
  scheduledDate?: Date;

  @IsOptional()
  @ValidateNested()
  @Type(() => GeolocationDto)
  geolocation?: GeolocationDto;
}

// dto/submit-inspection.dto.ts
export class SubmitInspectionDto {
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => InspectionResponseDto)
  responses: InspectionResponseDto[];

  @IsOptional()
  @IsString()
  generalObservations?: string;

  @IsOptional()
  @IsArray()
  @IsString({ each: true })
  attachments?: string[];

  @IsOptional()
  @IsString()
  signature?: string;
}

class InspectionResponseDto {
  @IsString()
  sectionId: string;

  @IsString()
  itemId: string;

  @IsString()
  response: string;

  @IsOptional()
  @IsBoolean()
  isNonCompliant?: boolean;

  @IsOptional()
  @IsEnum(['critical', 'major', 'minor', 'observation'])
  severity?: string;

  @IsOptional()
  @IsString()
  notes?: string;

  @IsOptional()
  @IsArray()
  @IsString({ each: true })
  evidenceUrls?: string[];
}

4.3. Incidents DTOs

// dto/create-incident.dto.ts
export class CreateIncidentDto {
  @IsUUID()
  projectId: string;

  @IsEnum(['accident', 'near_miss', 'unsafe_condition', 'property_damage', 'environmental'])
  type: string;

  @IsEnum(['fatal', 'serious', 'moderate', 'minor', 'first_aid'])
  severity: string;

  @Type(() => Date)
  @IsDate()
  incidentDate: Date;

  @IsString()
  incidentTime: string;

  @IsString()
  location: string;

  @IsOptional()
  @IsUUID()
  areaId?: string;

  @IsString()
  description: string;

  @IsOptional()
  @IsString()
  immediateActions?: string;

  @IsOptional()
  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => AffectedPersonDto)
  affectedPersons?: AffectedPersonDto[];

  @IsOptional()
  @IsArray()
  @IsString({ each: true })
  evidenceUrls?: string[];

  @IsOptional()
  @ValidateNested()
  @Type(() => GeolocationDto)
  geolocation?: GeolocationDto;
}

class AffectedPersonDto {
  @IsUUID()
  workerId: string;

  @IsString()
  name: string;

  @IsString()
  role: string;

  @IsOptional()
  @IsString()
  injuryType?: string;

  @IsOptional()
  @IsString()
  injuryLocation?: string;

  @IsOptional()
  @IsBoolean()
  hospitalizedRequired?: boolean;
}

// dto/create-investigation.dto.ts
export class CreateInvestigationDto {
  @IsUUID()
  incidentId: string;

  @IsUUID()
  investigatorId: string;

  @Type(() => Date)
  @IsDate()
  investigationDate: Date;

  @IsOptional()
  @IsString()
  findings?: string;

  @IsOptional()
  @IsString()
  contributingFactors?: string;
}

// dto/complete-investigation.dto.ts
export class CompleteInvestigationDto {
  @IsString()
  findings: string;

  @IsString()
  contributingFactors: string;

  @ValidateNested()
  @Type(() => RootCauseAnalysisDto)
  rootCauseAnalysis: RootCauseAnalysisDto;

  @IsString()
  recommendations: string;

  @IsArray()
  @ValidateNested({ each: true })
  @Type(() => CreateCorrectiveActionDto)
  correctiveActions: CreateCorrectiveActionDto[];
}

class RootCauseAnalysisDto {
  @IsEnum(['5why', 'fishbone', 'fta'])
  method: string;

  @IsObject()
  analysis: any;

  @IsArray()
  @IsString({ each: true })
  rootCauses: string[];
}

// dto/create-corrective-action.dto.ts
export class CreateCorrectiveActionDto {
  @IsString()
  description: string;

  @IsEnum(['immediate', 'corrective', 'preventive'])
  actionType: string;

  @IsEnum(['critical', 'high', 'medium', 'low'])
  priority: string;

  @IsUUID()
  responsibleId: string;

  @Type(() => Date)
  @IsDate()
  dueDate: Date;
}

4.4. Trainings DTOs

// dto/create-training.dto.ts
export class CreateSafetyTrainingDto {
  @IsUUID()
  courseId: string;

  @IsOptional()
  @IsUUID()
  projectId?: string;

  @Type(() => Date)
  @IsDate()
  trainingDate: Date;

  @IsString()
  startTime: string;

  @IsString()
  endTime: string;

  @IsString()
  location: string;

  @IsUUID()
  instructorId: string;

  @IsOptional()
  @IsString()
  externalInstructor?: string;

  @IsNumber()
  @Min(1)
  capacity: number;

  @IsOptional()
  @IsString()
  materials?: string;

  @IsOptional()
  @IsString()
  notes?: string;
}

// dto/register-attendance.dto.ts
export class RegisterAttendanceDto {
  @IsUUID()
  trainingId: string;

  @IsArray()
  @IsUUID({}, { each: true })
  workerIds: string[];
}

// dto/complete-attendance.dto.ts
export class CompleteAttendanceDto {
  @IsBoolean()
  attended: boolean;

  @IsOptional()
  @IsNumber()
  @Min(0)
  @Max(100)
  score?: number;

  @IsOptional()
  @IsString()
  signatureUrl?: string;

  @IsOptional()
  @IsString()
  notes?: string;
}

5. Services - Lógica de Negocio

5.1. EPP Service

// services/epp.service.ts
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { EPPItem, EPPAssignment, EPPInventory } from '../entities';

@Injectable()
export class EPPService {
  constructor(
    @InjectRepository(EPPItem)
    private eppItemRepository: Repository<EPPItem>,
    @InjectRepository(EPPInventory)
    private inventoryRepository: Repository<EPPInventory>
  ) {}

  async findAll(filters?: any): Promise<EPPItem[]> {
    const query = this.eppItemRepository.createQueryBuilder('epp')
      .leftJoinAndSelect('epp.category', 'category')
      .where('epp.isActive = :isActive', { isActive: true });

    if (filters?.categoryId) {
      query.andWhere('epp.categoryId = :categoryId', { categoryId: filters.categoryId });
    }

    if (filters?.search) {
      query.andWhere('(epp.name ILIKE :search OR epp.code ILIKE :search)', {
        search: `%${filters.search}%`
      });
    }

    return query.getMany();
  }

  async findOne(id: string): Promise<EPPItem> {
    const item = await this.eppItemRepository.findOne({
      where: { id },
      relations: ['category']
    });

    if (!item) {
      throw new NotFoundException(`EPP item ${id} not found`);
    }

    return item;
  }

  async checkAvailability(eppItemId: string, projectId: string, quantity: number): Promise<boolean> {
    const inventory = await this.inventoryRepository.findOne({
      where: { eppItemId, projectId }
    });

    if (!inventory) {
      return false;
    }

    const available = inventory.quantity - inventory.reserved;
    return available >= quantity;
  }

  async getInventoryByProject(projectId: string): Promise<EPPInventory[]> {
    return this.inventoryRepository.find({
      where: { projectId },
      relations: ['eppItem', 'eppItem.category']
    });
  }

  async getLowStockItems(projectId?: string): Promise<EPPInventory[]> {
    const query = this.inventoryRepository.createQueryBuilder('inv')
      .leftJoinAndSelect('inv.eppItem', 'epp')
      .where('inv.quantity <= inv.minimumStock');

    if (projectId) {
      query.andWhere('inv.projectId = :projectId', { projectId });
    }

    return query.getMany();
  }
}

// services/epp-assignment.service.ts
@Injectable()
export class EPPAssignmentService {
  constructor(
    @InjectRepository(EPPAssignment)
    private assignmentRepository: Repository<EPPAssignment>,
    @InjectRepository(EPPInventory)
    private inventoryRepository: Repository<EPPInventory>,
    private eppService: EPPService,
    private notificationsService: NotificationsService
  ) {}

  async assignEPP(dto: CreateEPPAssignmentDto, userId: string): Promise<EPPAssignment> {
    // Verificar disponibilidad
    const isAvailable = await this.eppService.checkAvailability(
      dto.eppItemId,
      dto.projectId,
      dto.quantity
    );

    if (!isAvailable) {
      throw new BadRequestException('Insufficient EPP inventory');
    }

    // Crear asignación
    const assignment = this.assignmentRepository.create({
      ...dto,
      assignedBy: userId,
      status: 'assigned'
    });

    // Reducir inventario
    await this.inventoryRepository.decrement(
      { eppItemId: dto.eppItemId, projectId: dto.projectId },
      'quantity',
      dto.quantity
    );

    const saved = await this.assignmentRepository.save(assignment);

    // Notificar al trabajador
    await this.notificationsService.send({
      userId: dto.workerId,
      type: 'epp_assigned',
      title: 'EPP Asignado',
      message: `Se te ha asignado equipo de protección personal`,
      data: { assignmentId: saved.id }
    });

    return saved;
  }

  async returnEPP(assignmentId: string, condition: string, userId: string): Promise<EPPAssignment> {
    const assignment = await this.assignmentRepository.findOne({
      where: { id: assignmentId }
    });

    if (!assignment) {
      throw new NotFoundException('Assignment not found');
    }

    assignment.actualReturnDate = new Date();
    assignment.condition = condition;
    assignment.receivedBy = userId;
    assignment.status = condition === 'good' ? 'returned' : 'damaged';

    // Si está en buenas condiciones, regresar al inventario
    if (condition === 'good') {
      await this.inventoryRepository.increment(
        { eppItemId: assignment.eppItemId, projectId: assignment.projectId },
        'quantity',
        assignment.quantity
      );
    }

    return this.assignmentRepository.save(assignment);
  }

  async getWorkerEPP(workerId: string, projectId: string): Promise<EPPAssignment[]> {
    return this.assignmentRepository.find({
      where: {
        workerId,
        projectId,
        status: 'assigned'
      },
      relations: ['eppItem', 'eppItem.category']
    });
  }

  async getExpiringEPP(days: number = 30): Promise<EPPAssignment[]> {
    const futureDate = new Date();
    futureDate.setDate(futureDate.getDate() + days);

    return this.assignmentRepository.createQueryBuilder('assignment')
      .leftJoinAndSelect('assignment.eppItem', 'epp')
      .where('assignment.status = :status', { status: 'assigned' })
      .andWhere('assignment.expectedReturnDate <= :futureDate', { futureDate })
      .getMany();
  }
}

5.2. Incidents Workflow Service

// workflows/incident-workflow.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Incident, IncidentInvestigation, CorrectiveAction } from '../entities';

@Injectable()
export class IncidentWorkflowService {
  constructor(
    @InjectRepository(Incident)
    private incidentRepository: Repository<Incident>,
    @InjectRepository(IncidentInvestigation)
    private investigationRepository: Repository<IncidentInvestigation>,
    @InjectRepository(CorrectiveAction)
    private actionRepository: Repository<CorrectiveAction>,
    private notificationsService: NotificationsService,
    private alertsService: AlertsService
  ) {}

  /**
   * Workflow Step 1: Reporte inicial del incidente
   */
  async reportIncident(dto: CreateIncidentDto, reportedBy: string): Promise<Incident> {
    const incidentNumber = await this.generateIncidentNumber();

    const incident = this.incidentRepository.create({
      ...dto,
      incidentNumber,
      reportedBy,
      reportedAt: new Date(),
      status: 'reported'
    });

    const saved = await this.incidentRepository.save(incident);

    // Determinar si requiere notificación a autoridades (STPS)
    if (this.requiresAuthorityNotification(saved)) {
      saved.requiresAuthority = true;
      await this.incidentRepository.save(saved);

      // Alerta inmediata
      await this.alertsService.sendCriticalAlert({
        type: 'incident_requires_authority',
        incidentId: saved.id,
        severity: saved.severity
      });
    }

    // Notificar a responsables de seguridad
    await this.notifySecurityTeam(saved);

    // Iniciar investigación automáticamente para incidentes graves
    if (['fatal', 'serious'].includes(saved.severity)) {
      await this.initiateInvestigation(saved.id);
    }

    return saved;
  }

  /**
   * Workflow Step 2: Iniciar investigación
   */
  async initiateInvestigation(incidentId: string, investigatorId?: string): Promise<IncidentInvestigation> {
    const incident = await this.incidentRepository.findOne({
      where: { id: incidentId }
    });

    if (!incident) {
      throw new NotFoundException('Incident not found');
    }

    // Asignar investigador automáticamente si no se especifica
    if (!investigatorId) {
      investigatorId = await this.assignInvestigator(incident);
    }

    const investigation = this.investigationRepository.create({
      incidentId,
      investigatorId,
      investigationDate: new Date(),
      status: 'initiated'
    });

    const saved = await this.investigationRepository.save(investigation);

    // Actualizar estado del incidente
    incident.status = 'investigating';
    await this.incidentRepository.save(incident);

    // Notificar al investigador
    await this.notificationsService.send({
      userId: investigatorId,
      type: 'investigation_assigned',
      title: 'Investigación de Incidente Asignada',
      message: `Se te ha asignado la investigación del incidente ${incident.incidentNumber}`,
      data: { incidentId, investigationId: saved.id },
      priority: 'high'
    });

    return saved;
  }

  /**
   * Workflow Step 3: Completar investigación con análisis de causa raíz
   */
  async completeInvestigation(
    investigationId: string,
    dto: CompleteInvestigationDto
  ): Promise<IncidentInvestigation> {
    const investigation = await this.investigationRepository.findOne({
      where: { id: investigationId },
      relations: ['incident']
    });

    if (!investigation) {
      throw new NotFoundException('Investigation not found');
    }

    // Actualizar investigación
    Object.assign(investigation, {
      findings: dto.findings,
      contributingFactors: dto.contributingFactors,
      rootCauseAnalysis: dto.rootCauseAnalysis,
      recommendations: dto.recommendations,
      status: 'completed',
      completedAt: new Date()
    });

    await this.investigationRepository.save(investigation);

    // Crear acciones correctivas
    for (const actionDto of dto.correctiveActions) {
      await this.createCorrectiveAction(investigationId, actionDto);
    }

    // Actualizar estado del incidente
    investigation.incident.status = 'corrective_actions';
    await this.incidentRepository.save(investigation.incident);

    return investigation;
  }

  /**
   * Workflow Step 4: Crear y asignar acciones correctivas
   */
  async createCorrectiveAction(
    investigationId: string,
    dto: CreateCorrectiveActionDto
  ): Promise<CorrectiveAction> {
    const actionNumber = await this.generateActionNumber();

    const action = this.actionRepository.create({
      ...dto,
      investigationId,
      actionNumber,
      status: 'pending'
    });

    const saved = await this.actionRepository.save(action);

    // Notificar al responsable
    await this.notificationsService.send({
      userId: dto.responsibleId,
      type: 'corrective_action_assigned',
      title: 'Acción Correctiva Asignada',
      message: `Se te ha asignado una acción correctiva: ${dto.description}`,
      data: {
        actionId: saved.id,
        dueDate: dto.dueDate,
        priority: dto.priority
      },
      priority: dto.priority === 'critical' ? 'high' : 'normal'
    });

    return saved;
  }

  /**
   * Workflow Step 5: Implementar acción correctiva
   */
  async implementAction(
    actionId: string,
    implementationNotes: string,
    evidenceUrls: string[]
  ): Promise<CorrectiveAction> {
    const action = await this.actionRepository.findOne({
      where: { id: actionId }
    });

    if (!action) {
      throw new NotFoundException('Corrective action not found');
    }

    action.status = 'completed';
    action.completionDate = new Date();
    action.implementationNotes = implementationNotes;
    action.evidenceUrls = evidenceUrls;

    return this.actionRepository.save(action);
  }

  /**
   * Workflow Step 6: Verificar acción correctiva
   */
  async verifyAction(
    actionId: string,
    verifiedBy: string,
    verificationNotes: string,
    approved: boolean
  ): Promise<CorrectiveAction> {
    const action = await this.actionRepository.findOne({
      where: { id: actionId },
      relations: ['investigation', 'investigation.incident']
    });

    if (!action) {
      throw new NotFoundException('Corrective action not found');
    }

    action.verifiedBy = verifiedBy;
    action.verificationDate = new Date();
    action.verificationNotes = verificationNotes;
    action.status = approved ? 'verified' : 'in_progress';

    await this.actionRepository.save(action);

    // Si todas las acciones están verificadas, cerrar el incidente
    const allActions = await this.actionRepository.find({
      where: { investigationId: action.investigationId }
    });

    const allVerified = allActions.every(a => a.status === 'verified');

    if (allVerified) {
      action.investigation.incident.status = 'closed';
      await this.incidentRepository.save(action.investigation.incident);
    }

    return action;
  }

  /**
   * Verifica si el incidente requiere notificación a autoridades según NOM-STPS
   */
  private requiresAuthorityNotification(incident: Incident): boolean {
    // NOM-019-STPS-2011: Notificar incidentes fatales o con días perdidos
    if (incident.severity === 'fatal') {
      return true;
    }

    if (incident.severity === 'serious' && incident.workDaysLost > 0) {
      return true;
    }

    return false;
  }

  /**
   * Asigna investigador basado en severidad y disponibilidad
   */
  private async assignInvestigator(incident: Incident): Promise<string> {
    // Lógica para asignar investigador
    // Por ahora retorna un ID dummy - implementar según estructura organizacional
    return 'safety-manager-id';
  }

  /**
   * Notifica al equipo de seguridad sobre nuevo incidente
   */
  private async notifySecurityTeam(incident: Incident): Promise<void> {
    await this.alertsService.sendToRole({
      role: 'safety_manager',
      type: 'new_incident',
      title: `Nuevo Incidente: ${incident.type}`,
      message: `Severidad: ${incident.severity} - ${incident.location}`,
      data: { incidentId: incident.id },
      priority: incident.severity === 'fatal' ? 'critical' : 'high'
    });
  }

  private async generateIncidentNumber(): Promise<string> {
    const year = new Date().getFullYear();
    const count = await this.incidentRepository.count({
      where: {
        incidentNumber: Like(`INC-${year}%`)
      }
    });
    return `INC-${year}-${String(count + 1).padStart(4, '0')}`;
  }

  private async generateActionNumber(): Promise<string> {
    const year = new Date().getFullYear();
    const count = await this.actionRepository.count({
      where: {
        actionNumber: Like(`AC-${year}%`)
      }
    });
    return `AC-${year}-${String(count + 1).padStart(4, '0')}`;
  }
}

5.3. Safety Inspections Service

// services/safety-inspections.service.ts
@Injectable()
export class SafetyInspectionsService {
  constructor(
    @InjectRepository(SafetyInspection)
    private inspectionRepository: Repository<SafetyInspection>,
    @InjectRepository(InspectionResponse)
    private responseRepository: Repository<InspectionResponse>,
    private notificationsService: NotificationsService
  ) {}

  async createInspection(dto: CreateSafetyInspectionDto, inspectorId: string): Promise<SafetyInspection> {
    const inspectionNumber = await this.generateInspectionNumber();

    const inspection = this.inspectionRepository.create({
      ...dto,
      inspectionNumber,
      inspectorId,
      status: 'in_progress'
    });

    return this.inspectionRepository.save(inspection);
  }

  async submitInspection(inspectionId: string, dto: SubmitInspectionDto): Promise<SafetyInspection> {
    const inspection = await this.inspectionRepository.findOne({
      where: { id: inspectionId },
      relations: ['template']
    });

    if (!inspection) {
      throw new NotFoundException('Inspection not found');
    }

    // Guardar respuestas
    const responses = dto.responses.map(r =>
      this.responseRepository.create({
        ...r,
        inspectionId
      })
    );

    await this.responseRepository.save(responses);

    // Calcular métricas
    const { score, findings } = this.calculateInspectionScore(responses, inspection.template);

    // Actualizar inspección
    inspection.status = 'completed';
    inspection.submittedAt = new Date();
    inspection.overallScore = score;
    inspection.criticalFindings = findings.critical;
    inspection.majorFindings = findings.major;
    inspection.minorFindings = findings.minor;
    inspection.generalObservations = dto.generalObservations;
    inspection.attachments = dto.attachments;
    inspection.signature = dto.signature;

    const saved = await this.inspectionRepository.save(inspection);

    // Generar alertas para hallazgos críticos
    if (findings.critical > 0) {
      await this.notificationsService.send({
        userId: 'safety-manager-id',
        type: 'critical_inspection_findings',
        title: 'Hallazgos Críticos en Inspección',
        message: `${findings.critical} hallazgos críticos en ${inspection.inspectionNumber}`,
        data: { inspectionId: saved.id },
        priority: 'high'
      });
    }

    return saved;
  }

  private calculateInspectionScore(
    responses: InspectionResponse[],
    template: InspectionTemplate
  ): { score: number; findings: { critical: number; major: number; minor: number } } {
    let totalPoints = 0;
    let earnedPoints = 0;
    const findings = { critical: 0, major: 0, minor: 0 };

    for (const response of responses) {
      totalPoints += 1;

      if (!response.isNonCompliant) {
        earnedPoints += 1;
      } else {
        if (response.severity === 'critical') findings.critical++;
        else if (response.severity === 'major') findings.major++;
        else if (response.severity === 'minor') findings.minor++;
      }
    }

    const score = totalPoints > 0 ? (earnedPoints / totalPoints) * 100 : 0;

    return { score, findings };
  }

  private async generateInspectionNumber(): Promise<string> {
    const year = new Date().getFullYear();
    const month = String(new Date().getMonth() + 1).padStart(2, '0');
    const count = await this.inspectionRepository.count({
      where: {
        inspectionNumber: Like(`INS-${year}${month}%`)
      }
    });
    return `INS-${year}${month}-${String(count + 1).padStart(4, '0')}`;
  }
}

6. Controllers - Endpoints API

6.1. EPP Controller

// controllers/epp.controller.ts
import { Controller, Get, Post, Put, Body, Param, Query, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '@/guards/jwt-auth.guard';
import { RolesGuard } from '@/guards/roles.guard';
import { Roles } from '@/decorators/roles.decorator';
import { CurrentUser } from '@/decorators/current-user.decorator';

@ApiTags('EPP - Equipos de Protección Personal')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('safety/epp')
export class EPPController {
  constructor(
    private readonly eppService: EPPService,
    private readonly assignmentService: EPPAssignmentService,
    private readonly inventoryService: EPPInventoryService
  ) {}

  @Get()
  @ApiOperation({ summary: 'Listar equipos EPP' })
  @Roles('safety_manager', 'warehouse_manager', 'supervisor')
  async findAll(@Query() filters: any) {
    return this.eppService.findAll(filters);
  }

  @Get('inventory/:projectId')
  @ApiOperation({ summary: 'Obtener inventario EPP por proyecto' })
  @Roles('safety_manager', 'warehouse_manager', 'supervisor')
  async getInventory(@Param('projectId') projectId: string) {
    return this.eppService.getInventoryByProject(projectId);
  }

  @Get('inventory/low-stock')
  @ApiOperation({ summary: 'Items con stock bajo' })
  @Roles('safety_manager', 'warehouse_manager')
  async getLowStock(@Query('projectId') projectId?: string) {
    return this.eppService.getLowStockItems(projectId);
  }

  @Post('assignments')
  @ApiOperation({ summary: 'Asignar EPP a trabajador' })
  @Roles('safety_manager', 'warehouse_manager', 'supervisor')
  async assignEPP(
    @Body() dto: CreateEPPAssignmentDto,
    @CurrentUser() user: any
  ) {
    return this.assignmentService.assignEPP(dto, user.id);
  }

  @Put('assignments/:id/return')
  @ApiOperation({ summary: 'Devolver EPP asignado' })
  @Roles('safety_manager', 'warehouse_manager')
  async returnEPP(
    @Param('id') id: string,
    @Body() body: { condition: string },
    @CurrentUser() user: any
  ) {
    return this.assignmentService.returnEPP(id, body.condition, user.id);
  }

  @Get('workers/:workerId/assignments')
  @ApiOperation({ summary: 'EPP asignado a trabajador - Para apps móviles' })
  @Roles('worker', 'supervisor', 'safety_manager')
  async getWorkerEPP(
    @Param('workerId') workerId: string,
    @Query('projectId') projectId: string
  ) {
    return this.assignmentService.getWorkerEPP(workerId, projectId);
  }

  @Get('assignments/expiring')
  @ApiOperation({ summary: 'EPP próximo a vencer' })
  @Roles('safety_manager', 'supervisor')
  async getExpiringEPP(@Query('days') days: number = 30) {
    return this.assignmentService.getExpiringEPP(days);
  }

  @Post('deliveries')
  @ApiOperation({ summary: 'Registrar entrega de EPP - Para MOB-003' })
  @Roles('warehouse_manager', 'safety_manager')
  async createDelivery(
    @Body() dto: CreateEPPDeliveryDto,
    @CurrentUser() user: any
  ) {
    return this.assignmentService.createDelivery(dto, user.id);
  }
}

6.2. Safety Inspections Controller

// controllers/safety-inspections.controller.ts
@ApiTags('Inspecciones de Seguridad')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('safety/inspections')
export class SafetyInspectionsController {
  constructor(
    private readonly inspectionsService: SafetyInspectionsService,
    private readonly templatesService: InspectionTemplatesService
  ) {}

  @Get('templates')
  @ApiOperation({ summary: 'Listar plantillas de inspección' })
  @Roles('safety_manager', 'supervisor', 'inspector')
  async getTemplates(@Query('type') type?: string) {
    return this.templatesService.findAll({ type });
  }

  @Get('templates/:id')
  @ApiOperation({ summary: 'Obtener plantilla - Para apps móviles MOB-003' })
  @Roles('safety_manager', 'supervisor', 'inspector')
  async getTemplate(@Param('id') id: string) {
    return this.templatesService.findOne(id);
  }

  @Post()
  @ApiOperation({ summary: 'Crear inspección - Desde app móvil MOB-003' })
  @Roles('safety_manager', 'supervisor', 'inspector')
  async create(
    @Body() dto: CreateSafetyInspectionDto,
    @CurrentUser() user: any
  ) {
    return this.inspectionsService.createInspection(dto, user.id);
  }

  @Put(':id/submit')
  @ApiOperation({ summary: 'Enviar inspección completada - Desde app móvil' })
  @Roles('safety_manager', 'supervisor', 'inspector')
  async submit(
    @Param('id') id: string,
    @Body() dto: SubmitInspectionDto
  ) {
    return this.inspectionsService.submitInspection(id, dto);
  }

  @Get('project/:projectId')
  @ApiOperation({ summary: 'Inspecciones por proyecto' })
  @Roles('safety_manager', 'supervisor', 'inspector')
  async getByProject(
    @Param('projectId') projectId: string,
    @Query() filters: any
  ) {
    return this.inspectionsService.findByProject(projectId, filters);
  }

  @Get(':id')
  @ApiOperation({ summary: 'Detalle de inspección' })
  @Roles('safety_manager', 'supervisor', 'inspector')
  async findOne(@Param('id') id: string) {
    return this.inspectionsService.findOne(id);
  }

  @Get('scheduled/pending')
  @ApiOperation({ summary: 'Inspecciones pendientes - Para MOB-003' })
  @Roles('safety_manager', 'supervisor', 'inspector')
  async getPending(@Query('projectId') projectId: string) {
    return this.inspectionsService.findPending(projectId);
  }
}

6.3. Incidents Controller

// controllers/incidents.controller.ts
@ApiTags('Incidentes y Accidentes')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('safety/incidents')
export class IncidentsController {
  constructor(
    private readonly incidentsService: IncidentsService,
    private readonly workflowService: IncidentWorkflowService,
    private readonly investigationService: IncidentInvestigationService
  ) {}

  @Post()
  @ApiOperation({ summary: 'Reportar incidente - Desde app móvil MOB-004' })
  @Roles('worker', 'supervisor', 'safety_manager')
  async report(
    @Body() dto: CreateIncidentDto,
    @CurrentUser() user: any
  ) {
    return this.workflowService.reportIncident(dto, user.id);
  }

  @Get()
  @ApiOperation({ summary: 'Listar incidentes' })
  @Roles('safety_manager', 'supervisor')
  async findAll(@Query() filters: any) {
    return this.incidentsService.findAll(filters);
  }

  @Get(':id')
  @ApiOperation({ summary: 'Detalle de incidente' })
  @Roles('safety_manager', 'supervisor', 'worker')
  async findOne(@Param('id') id: string) {
    return this.incidentsService.findOne(id);
  }

  @Post(':id/investigation')
  @ApiOperation({ summary: 'Iniciar investigación' })
  @Roles('safety_manager')
  async initiateInvestigation(
    @Param('id') id: string,
    @Body() body: { investigatorId?: string }
  ) {
    return this.workflowService.initiateInvestigation(id, body.investigatorId);
  }

  @Put('investigations/:id/complete')
  @ApiOperation({ summary: 'Completar investigación con análisis' })
  @Roles('safety_manager', 'investigator')
  async completeInvestigation(
    @Param('id') id: string,
    @Body() dto: CompleteInvestigationDto
  ) {
    return this.workflowService.completeInvestigation(id, dto);
  }

  @Post('investigations/:id/corrective-actions')
  @ApiOperation({ summary: 'Agregar acción correctiva' })
  @Roles('safety_manager')
  async addCorrectiveAction(
    @Param('id') investigationId: string,
    @Body() dto: CreateCorrectiveActionDto
  ) {
    return this.workflowService.createCorrectiveAction(investigationId, dto);
  }

  @Put('corrective-actions/:id/implement')
  @ApiOperation({ summary: 'Implementar acción correctiva' })
  @Roles('supervisor', 'safety_manager')
  async implementAction(
    @Param('id') id: string,
    @Body() body: { implementationNotes: string; evidenceUrls: string[] }
  ) {
    return this.workflowService.implementAction(
      id,
      body.implementationNotes,
      body.evidenceUrls
    );
  }

  @Put('corrective-actions/:id/verify')
  @ApiOperation({ summary: 'Verificar acción correctiva' })
  @Roles('safety_manager')
  async verifyAction(
    @Param('id') id: string,
    @Body() body: { verificationNotes: string; approved: boolean },
    @CurrentUser() user: any
  ) {
    return this.workflowService.verifyAction(
      id,
      user.id,
      body.verificationNotes,
      body.approved
    );
  }

  @Get('project/:projectId/statistics')
  @ApiOperation({ summary: 'Estadísticas de incidentes por proyecto' })
  @Roles('safety_manager', 'supervisor')
  async getStatistics(@Param('projectId') projectId: string) {
    return this.incidentsService.getStatistics(projectId);
  }

  @Post(':id/witnesses')
  @ApiOperation({ summary: 'Agregar testigo a incidente' })
  @Roles('safety_manager', 'investigator')
  async addWitness(
    @Param('id') id: string,
    @Body() dto: CreateWitnessDto
  ) {
    return this.incidentsService.addWitness(id, dto);
  }
}

6.4. Safety Trainings Controller

// controllers/safety-trainings.controller.ts
@ApiTags('Capacitaciones de Seguridad')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard, RolesGuard)
@Controller('safety/trainings')
export class SafetyTrainingsController {
  constructor(
    private readonly trainingsService: SafetyTrainingsService,
    private readonly certificationsService: TrainingCertificationsService
  ) {}

  @Get('courses')
  @ApiOperation({ summary: 'Listar cursos de capacitación' })
  async getCourses(@Query() filters: any) {
    return this.trainingsService.getCourses(filters);
  }

  @Post()
  @ApiOperation({ summary: 'Programar capacitación' })
  @Roles('safety_manager', 'hr_manager')
  async schedule(@Body() dto: CreateSafetyTrainingDto) {
    return this.trainingsService.scheduleTraining(dto);
  }

  @Get()
  @ApiOperation({ summary: 'Listar capacitaciones' })
  @Roles('safety_manager', 'supervisor')
  async findAll(@Query() filters: any) {
    return this.trainingsService.findAll(filters);
  }

  @Get(':id')
  @ApiOperation({ summary: 'Detalle de capacitación' })
  async findOne(@Param('id') id: string) {
    return this.trainingsService.findOne(id);
  }

  @Post(':id/attendances')
  @ApiOperation({ summary: 'Registrar asistentes' })
  @Roles('safety_manager', 'instructor')
  async registerAttendees(
    @Param('id') id: string,
    @Body() dto: RegisterAttendanceDto
  ) {
    return this.trainingsService.registerAttendances(id, dto);
  }

  @Put('attendances/:id')
  @ApiOperation({ summary: 'Completar asistencia y calificación' })
  @Roles('safety_manager', 'instructor')
  async completeAttendance(
    @Param('id') id: string,
    @Body() dto: CompleteAttendanceDto
  ) {
    return this.trainingsService.completeAttendance(id, dto);
  }

  @Post(':id/complete')
  @ApiOperation({ summary: 'Finalizar capacitación y generar certificados' })
  @Roles('safety_manager')
  async completeTraining(@Param('id') id: string) {
    return this.trainingsService.completeTraining(id);
  }

  @Get('workers/:workerId/certifications')
  @ApiOperation({ summary: 'Certificaciones de trabajador - Para MOB-003/MOB-004' })
  async getWorkerCertifications(@Param('workerId') workerId: string) {
    return this.certificationsService.getWorkerCertifications(workerId);
  }

  @Get('workers/:workerId/expired')
  @ApiOperation({ summary: 'Certificaciones vencidas o próximas a vencer' })
  @Roles('safety_manager', 'hr_manager')
  async getExpiredCertifications(
    @Param('workerId') workerId: string,
    @Query('days') days: number = 30
  ) {
    return this.certificationsService.getExpiring(workerId, days);
  }

  @Get('project/:projectId/compliance')
  @ApiOperation({ summary: 'Cumplimiento de capacitaciones por proyecto' })
  @Roles('safety_manager', 'supervisor')
  async getComplianceReport(@Param('projectId') projectId: string) {
    return this.trainingsService.getComplianceReport(projectId);
  }
}

7. Sistema de Alertas y Notificaciones

7.1. Alerts Service

// shared/alerts/alerts.service.ts
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class AlertsService {
  constructor(
    private eventEmitter: EventEmitter2,
    private notificationsService: NotificationsService,
    private emailService: EmailService,
    private smsService: SMSService
  ) {}

  /**
   * Envía alerta crítica (incidentes fatales, condiciones peligrosas)
   */
  async sendCriticalAlert(data: {
    type: string;
    incidentId?: string;
    inspectionId?: string;
    severity: string;
    description?: string;
  }): Promise<void> {
    // Notificación push
    await this.notificationsService.sendToRole({
      role: 'safety_manager',
      type: data.type,
      title: 'ALERTA CRÍTICA DE SEGURIDAD',
      message: this.buildAlertMessage(data),
      data,
      priority: 'critical'
    });

    // Email
    await this.emailService.sendToRole({
      role: 'safety_manager',
      subject: 'ALERTA CRÍTICA DE SEGURIDAD',
      template: 'critical-safety-alert',
      data
    });

    // SMS para incidentes fatales
    if (data.severity === 'fatal') {
      await this.smsService.sendToRole({
        role: 'safety_manager',
        message: this.buildSMSAlert(data)
      });
    }

    // Emitir evento para integraciones
    this.eventEmitter.emit('safety.critical-alert', data);
  }

  /**
   * Alertas de EPP
   */
  async alertLowStock(inventory: EPPInventory): Promise<void> {
    await this.notificationsService.sendToRole({
      role: 'warehouse_manager',
      type: 'epp_low_stock',
      title: 'Stock Bajo de EPP',
      message: `${inventory.eppItem.name}: ${inventory.quantity} unidades`,
      data: { inventoryId: inventory.id },
      priority: 'normal'
    });
  }

  async alertExpiringEPP(assignments: EPPAssignment[]): Promise<void> {
    for (const assignment of assignments) {
      await this.notificationsService.send({
        userId: assignment.workerId,
        type: 'epp_expiring',
        title: 'EPP Próximo a Vencer',
        message: `Tu ${assignment.eppItem.name} debe devolverse el ${assignment.expectedReturnDate}`,
        data: { assignmentId: assignment.id },
        priority: 'normal'
      });
    }
  }

  /**
   * Alertas de inspecciones
   */
  async alertOverdueInspection(inspection: SafetyInspection): Promise<void> {
    await this.notificationsService.send({
      userId: inspection.inspectorId,
      type: 'inspection_overdue',
      title: 'Inspección Pendiente',
      message: `Inspección ${inspection.inspectionNumber} está vencida`,
      data: { inspectionId: inspection.id },
      priority: 'high'
    });
  }

  /**
   * Alertas de acciones correctivas
   */
  async alertOverdueAction(action: CorrectiveAction): Promise<void> {
    await this.notificationsService.send({
      userId: action.responsibleId,
      type: 'action_overdue',
      title: 'Acción Correctiva Vencida',
      message: `${action.description} - Vencimiento: ${action.dueDate}`,
      data: { actionId: action.id },
      priority: 'high'
    });

    // Notificar también al supervisor
    await this.notificationsService.sendToRole({
      role: 'safety_manager',
      type: 'action_overdue_escalation',
      title: 'Acción Correctiva Vencida',
      message: `Acción ${action.actionNumber} está vencida`,
      data: { actionId: action.id },
      priority: 'high'
    });
  }

  /**
   * Alertas de capacitaciones
   */
  async alertExpiringCertifications(certifications: TrainingCertification[]): Promise<void> {
    for (const cert of certifications) {
      await this.notificationsService.send({
        userId: cert.workerId,
        type: 'certification_expiring',
        title: 'Certificación Próxima a Vencer',
        message: `Tu certificación ${cert.course.name} vence el ${cert.expirationDate}`,
        data: { certificationId: cert.id },
        priority: 'normal'
      });
    }
  }

  private buildAlertMessage(data: any): string {
    const messages = {
      'incident_requires_authority': 'Incidente que requiere notificación a STPS',
      'critical_inspection_findings': 'Hallazgos críticos en inspección',
      'unsafe_condition': 'Condición insegura detectada'
    };

    return messages[data.type] || 'Alerta de seguridad';
  }

  private buildSMSAlert(data: any): string {
    return `ALERTA CRÍTICA: ${data.type} - Severidad: ${data.severity}. Revisar sistema inmediatamente.`;
  }
}

7.2. Scheduled Jobs para Alertas

// shared/alerts/alert-jobs.service.ts
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class AlertJobsService {
  constructor(
    private alertsService: AlertsService,
    private eppService: EPPService,
    private certificationsService: TrainingCertificationsService,
    private actionsService: CorrectiveActionsService
  ) {}

  /**
   * Diario: Verificar EPP bajo en stock
   */
  @Cron(CronExpression.EVERY_DAY_AT_8AM)
  async checkLowStock(): Promise<void> {
    const lowStock = await this.eppService.getLowStockItems();

    for (const item of lowStock) {
      await this.alertsService.alertLowStock(item);
    }
  }

  /**
   * Diario: Verificar EPP próximo a vencer
   */
  @Cron(CronExpression.EVERY_DAY_AT_9AM)
  async checkExpiringEPP(): Promise<void> {
    const expiring = await this.eppService.getExpiringEPP(7); // 7 días

    if (expiring.length > 0) {
      await this.alertsService.alertExpiringEPP(expiring);
    }
  }

  /**
   * Diario: Verificar certificaciones próximas a vencer
   */
  @Cron(CronExpression.EVERY_DAY_AT_10AM)
  async checkExpiringCertifications(): Promise<void> {
    const expiring = await this.certificationsService.getExpiringCertifications(30);

    if (expiring.length > 0) {
      await this.alertsService.alertExpiringCertifications(expiring);
    }
  }

  /**
   * Diario: Verificar acciones correctivas vencidas
   */
  @Cron(CronExpression.EVERY_DAY_AT_11AM)
  async checkOverdueActions(): Promise<void> {
    const overdue = await this.actionsService.getOverdueActions();

    for (const action of overdue) {
      await this.alertsService.alertOverdueAction(action);
    }
  }
}

8. Reportes para Cumplimiento NOM-STPS

8.1. Reports Service

// shared/reporting/safety-reports.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between } from 'typeorm';

@Injectable()
export class SafetyReportsService {
  constructor(
    @InjectRepository(Incident)
    private incidentRepository: Repository<Incident>,
    @InjectRepository(SafetyInspection)
    private inspectionRepository: Repository<SafetyInspection>,
    @InjectRepository(TrainingCertification)
    private certificationRepository: Repository<TrainingCertification>,
    private pdfService: PDFService
  ) {}

  /**
   * NOM-019-STPS-2011: Reporte de accidentes y enfermedades de trabajo
   */
  async generateNOM019Report(projectId: string, startDate: Date, endDate: Date): Promise<Buffer> {
    const incidents = await this.incidentRepository.find({
      where: {
        projectId,
        incidentDate: Between(startDate, endDate),
        type: 'accident'
      },
      relations: ['investigations', 'investigations.correctiveActions']
    });

    const data = {
      period: { startDate, endDate },
      totalIncidents: incidents.length,
      byType: this.groupIncidentsByType(incidents),
      bySeverity: this.groupIncidentsBySeverity(incidents),
      workDaysLost: incidents.reduce((sum, i) => sum + i.workDaysLost, 0),
      incidentRate: this.calculateIncidentRate(incidents, projectId),
      severityRate: this.calculateSeverityRate(incidents, projectId),
      details: incidents.map(i => ({
        incidentNumber: i.incidentNumber,
        date: i.incidentDate,
        type: i.type,
        severity: i.severity,
        location: i.location,
        description: i.description,
        workDaysLost: i.workDaysLost,
        investigated: i.investigations.length > 0,
        correctiveActions: i.investigations[0]?.correctiveActions.length || 0
      }))
    };

    return this.pdfService.generate('nom-019-report', data);
  }

  /**
   * NOM-030-STPS-2009: Servicios preventivos de seguridad y salud
   */
  async generateNOM030Report(projectId: string, year: number): Promise<Buffer> {
    const startDate = new Date(year, 0, 1);
    const endDate = new Date(year, 11, 31);

    const [inspections, trainings, incidents] = await Promise.all([
      this.inspectionRepository.count({
        where: { projectId, inspectionDate: Between(startDate, endDate) }
      }),
      this.certificationRepository
        .createQueryBuilder('cert')
        .where('cert.issueDate BETWEEN :startDate AND :endDate', { startDate, endDate })
        .getCount(),
      this.incidentRepository.count({
        where: { projectId, incidentDate: Between(startDate, endDate) }
      })
    ]);

    const data = {
      year,
      preventiveActivities: {
        inspections,
        trainings,
        incidents
      },
      complianceStatus: this.calculateComplianceStatus(projectId),
      recommendations: await this.generateRecommendations(projectId)
    };

    return this.pdfService.generate('nom-030-report', data);
  }

  /**
   * Reporte de capacitaciones impartidas
   */
  async generateTrainingReport(projectId: string, startDate: Date, endDate: Date): Promise<Buffer> {
    const trainings = await this.trainingRepository.find({
      where: {
        projectId,
        trainingDate: Between(startDate, endDate),
        status: 'completed'
      },
      relations: ['course', 'attendances']
    });

    const data = {
      period: { startDate, endDate },
      totalTrainings: trainings.length,
      totalAttendees: trainings.reduce((sum, t) => sum + t.attendances.length, 0),
      byCategory: this.groupTrainingsByCategory(trainings),
      certificationIssued: trainings.reduce((sum, t) =>
        sum + t.attendances.filter(a => a.passed).length, 0
      ),
      details: trainings.map(t => ({
        trainingNumber: t.trainingNumber,
        course: t.course.name,
        date: t.trainingDate,
        attendees: t.attendances.length,
        approved: t.attendances.filter(a => a.passed).length,
        averageScore: this.calculateAverageScore(t.attendances)
      }))
    };

    return this.pdfService.generate('training-report', data);
  }

  /**
   * Reporte de inspecciones de seguridad
   */
  async generateInspectionReport(projectId: string, startDate: Date, endDate: Date): Promise<Buffer> {
    const inspections = await this.inspectionRepository.find({
      where: {
        projectId,
        inspectionDate: Between(startDate, endDate),
        status: 'completed'
      },
      relations: ['template', 'responses']
    });

    const data = {
      period: { startDate, endDate },
      totalInspections: inspections.length,
      averageScore: this.calculateAverageScore(inspections.map(i => ({ score: i.overallScore }))),
      findings: {
        critical: inspections.reduce((sum, i) => sum + i.criticalFindings, 0),
        major: inspections.reduce((sum, i) => sum + i.majorFindings, 0),
        minor: inspections.reduce((sum, i) => sum + i.minorFindings, 0)
      },
      byType: this.groupInspectionsByType(inspections),
      details: inspections.map(i => ({
        inspectionNumber: i.inspectionNumber,
        date: i.inspectionDate,
        type: i.template.inspectionType,
        score: i.overallScore,
        findings: i.criticalFindings + i.majorFindings + i.minorFindings
      }))
    };

    return this.pdfService.generate('inspection-report', data);
  }

  /**
   * Dashboard ejecutivo de seguridad
   */
  async getExecutiveDashboard(projectId: string, month: number, year: number): Promise<any> {
    const startDate = new Date(year, month - 1, 1);
    const endDate = new Date(year, month, 0);

    const [incidents, inspections, trainings] = await Promise.all([
      this.incidentRepository.count({
        where: { projectId, incidentDate: Between(startDate, endDate) }
      }),
      this.inspectionRepository.count({
        where: { projectId, inspectionDate: Between(startDate, endDate), status: 'completed' }
      }),
      this.trainingRepository.count({
        where: { projectId, trainingDate: Between(startDate, endDate), status: 'completed' }
      })
    ]);

    return {
      period: { month, year },
      kpis: {
        incidentRate: await this.calculateIncidentRate(projectId, startDate, endDate),
        inspectionScore: await this.getAverageInspectionScore(projectId, startDate, endDate),
        trainingCompliance: await this.getTrainingCompliance(projectId),
        zeroIncidentDays: await this.getZeroIncidentDays(projectId, startDate, endDate)
      },
      summary: {
        incidents,
        inspections,
        trainings
      },
      trends: await this.getTrends(projectId, year)
    };
  }

  // Métodos auxiliares
  private groupIncidentsByType(incidents: Incident[]) {
    return incidents.reduce((acc, i) => {
      acc[i.type] = (acc[i.type] || 0) + 1;
      return acc;
    }, {});
  }

  private groupIncidentsBySeverity(incidents: Incident[]) {
    return incidents.reduce((acc, i) => {
      acc[i.severity] = (acc[i.severity] || 0) + 1;
      return acc;
    }, {});
  }

  private async calculateIncidentRate(projectId: string, startDate: Date, endDate: Date): Promise<number> {
    // Tasa de incidentes = (Número de incidentes / Horas trabajadas) * 200,000
    // 200,000 = 100 trabajadores trabajando 40 horas/semana durante 50 semanas
    const incidents = await this.incidentRepository.count({
      where: { projectId, incidentDate: Between(startDate, endDate) }
    });

    // Obtener horas trabajadas del proyecto (desde módulo de asistencia)
    const hoursWorked = await this.getProjectHoursWorked(projectId, startDate, endDate);

    return hoursWorked > 0 ? (incidents / hoursWorked) * 200000 : 0;
  }

  private async calculateSeverityRate(projectId: string, startDate: Date, endDate: Date): Promise<number> {
    const incidents = await this.incidentRepository.find({
      where: { projectId, incidentDate: Between(startDate, endDate) }
    });

    const totalDaysLost = incidents.reduce((sum, i) => sum + i.workDaysLost, 0);
    const hoursWorked = await this.getProjectHoursWorked(projectId, startDate, endDate);

    return hoursWorked > 0 ? (totalDaysLost / hoursWorked) * 200000 : 0;
  }

  private async getProjectHoursWorked(projectId: string, startDate: Date, endDate: Date): Promise<number> {
    // Integración con módulo de asistencia/nómina
    // Por ahora retornar valor estimado
    return 10000; // Placeholder
  }

  private calculateAverageScore(items: any[]): number {
    if (items.length === 0) return 0;
    const sum = items.reduce((acc, item) => acc + (item.score || 0), 0);
    return sum / items.length;
  }
}

9. Configuración y Buenas Prácticas

9.1. Validación Global

// main.ts
import { ValidationPipe } from '@nestjs/common';

app.useGlobalPipes(
  new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    transform: true,
    transformOptions: {
      enableImplicitConversion: true
    }
  })
);

9.2. Exception Filters

// filters/safety-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';

@Catch()
export class SafetyExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status = exception instanceof HttpException
      ? exception.getStatus()
      : 500;

    // Log crítico para errores en módulo de seguridad
    if (request.path.includes('/safety/')) {
      console.error('SAFETY MODULE ERROR:', {
        path: request.path,
        method: request.method,
        error: exception
      });
    }

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception instanceof HttpException ? exception.message : 'Internal server error'
    });
  }
}

9.3. Interceptor de Auditoría

// interceptors/audit.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class AuditInterceptor implements NestInterceptor {
  constructor(private auditService: AuditService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const { method, url, user, body } = request;

    // Auditar operaciones críticas de seguridad
    const criticalOperations = [
      '/safety/incidents',
      '/safety/inspections',
      '/safety/epp/assignments'
    ];

    const isCritical = criticalOperations.some(op => url.includes(op));

    return next.handle().pipe(
      tap((data) => {
        if (isCritical) {
          this.auditService.log({
            module: 'safety',
            action: method,
            endpoint: url,
            userId: user?.id,
            data: body,
            timestamp: new Date()
          });
        }
      })
    );
  }
}

10. Pruebas Unitarias

10.1. EPP Service Test

// epp.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { EPPService } from './epp.service';
import { EPPItem, EPPInventory } from './entities';

describe('EPPService', () => {
  let service: EPPService;
  let eppItemRepository: any;
  let inventoryRepository: any;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        EPPService,
        {
          provide: getRepositoryToken(EPPItem),
          useValue: {
            find: jest.fn(),
            findOne: jest.fn(),
            create: jest.fn(),
            save: jest.fn()
          }
        },
        {
          provide: getRepositoryToken(EPPInventory),
          useValue: {
            find: jest.fn(),
            findOne: jest.fn(),
            decrement: jest.fn()
          }
        }
      ]
    }).compile();

    service = module.get<EPPService>(EPPService);
    eppItemRepository = module.get(getRepositoryToken(EPPItem));
    inventoryRepository = module.get(getRepositoryToken(EPPInventory));
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('checkAvailability', () => {
    it('should return true if sufficient inventory', async () => {
      inventoryRepository.findOne.mockResolvedValue({
        quantity: 100,
        reserved: 20
      });

      const result = await service.checkAvailability('epp-id', 'project-id', 50);
      expect(result).toBe(true);
    });

    it('should return false if insufficient inventory', async () => {
      inventoryRepository.findOne.mockResolvedValue({
        quantity: 100,
        reserved: 90
      });

      const result = await service.checkAvailability('epp-id', 'project-id', 50);
      expect(result).toBe(false);
    });
  });
});

11. Documentación API

11.1. Swagger Configuration

// main.ts
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

const config = new DocumentBuilder()
  .setTitle('API de Seguridad Industrial')
  .setDescription('Módulo de gestión de seguridad para construcción')
  .setVersion('1.0')
  .addTag('EPP', 'Equipos de Protección Personal')
  .addTag('Inspecciones', 'Inspecciones de Seguridad')
  .addTag('Incidentes', 'Gestión de Incidentes y Accidentes')
  .addTag('Capacitaciones', 'Capacitaciones de Seguridad')
  .addBearerAuth()
  .build();

const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api/docs/safety', app, document);

12. Notas de Implementación

12.1. Prioridades de Desarrollo

  1. Fase 1 - Core (Sprint 1-2)

    • Entities y DTOs básicos
    • EPP Module (asignación y devolución)
    • Incidents Module (reporte básico)
    • Endpoints para apps móviles MOB-003 y MOB-004
  2. Fase 2 - Workflows (Sprint 3-4)

    • Workflow completo de investigación de incidentes
    • Safety Inspections Module
    • Sistema de alertas básico
  3. Fase 3 - Avanzado (Sprint 5-6)

    • Safety Trainings Module
    • Reportes NOM-STPS
    • Dashboard ejecutivo

12.2. Integraciones Requeridas

  • Módulo de RH: Obtener datos de trabajadores
  • Módulo de Proyectos: Validar proyectos y áreas
  • Módulo de Asistencia: Calcular horas trabajadas para métricas
  • Storage Service: Almacenar fotos, firmas digitales, PDFs
  • Notification Service: Push notifications para apps móviles

12.3. Consideraciones de Seguridad

  • Encriptar firmas digitales
  • Logs de auditoría para todas las operaciones
  • Backup diario de incidentes y evidencias
  • Restricción de eliminación de registros (soft delete)

Documento: ET-SEG-001-backend.md Versión: 1.0.0 Última Actualización: 2025-12-06