1975 lines
70 KiB
Markdown
1975 lines
70 KiB
Markdown
# ET-SEG-001: Especificación Técnica - Base de Datos
|
|
|
|
**Módulo:** MAI-007 - Seguridad Industrial
|
|
**Versión:** 1.0.0
|
|
**Fecha:** 2025-12-06
|
|
**Responsable:** Database Agent
|
|
|
|
---
|
|
|
|
## Resumen Ejecutivo
|
|
|
|
Este documento especifica el diseño completo de la base de datos para el módulo de Seguridad Industrial (MAI-007) del proyecto de construcción. El schema `safety_management` gestiona EPP (Equipo de Protección Personal), inspecciones de seguridad, incidentes laborales, investigaciones y capacitaciones.
|
|
|
|
---
|
|
|
|
## Contexto del Stack
|
|
|
|
```yaml
|
|
motor: PostgreSQL 15+
|
|
extensiones:
|
|
- uuid-ossp # Generación de UUIDs
|
|
- pg_trgm # Búsqueda fuzzy por trigram
|
|
- btree_gist # Índices GiST para exclusión
|
|
- pgcrypto # Funciones criptográficas
|
|
- postgis # Geolocalización GPS (opcional)
|
|
orm: Prisma 5.x
|
|
migraciones: Prisma migrations
|
|
schema: safety_management
|
|
```
|
|
|
|
---
|
|
|
|
## Schema Principal: safety_management
|
|
|
|
### Justificación
|
|
- **Separación de responsabilidades:** Aísla toda la funcionalidad de seguridad industrial
|
|
- **Multi-tenant:** Todas las tablas incluyen `tenant_id` para aislamiento de constructoras
|
|
- **Auditoría completa:** Registro de todas las operaciones críticas de seguridad
|
|
- **Normativas:** Cumple con NOM-031-STPS-2011 y NOM-017-STPS-2008
|
|
|
|
---
|
|
|
|
## ENUMs
|
|
|
|
### 1. epp_type (Tipos de EPP)
|
|
```sql
|
|
CREATE TYPE safety_management.epp_type AS ENUM (
|
|
'helmet', -- Casco
|
|
'safety_glasses', -- Lentes de seguridad
|
|
'goggles', -- Gafas protectoras
|
|
'face_shield', -- Careta
|
|
'ear_plugs', -- Tapones auditivos
|
|
'ear_muffs', -- Orejeras
|
|
'respirator', -- Respirador
|
|
'dust_mask', -- Cubrebocas
|
|
'work_gloves', -- Guantes de trabajo
|
|
'cut_resistant_gloves', -- Guantes anticorte
|
|
'electrical_gloves', -- Guantes dieléctricos
|
|
'safety_boots', -- Botas de seguridad
|
|
'rubber_boots', -- Botas de hule
|
|
'safety_vest', -- Chaleco de seguridad
|
|
'reflective_vest', -- Chaleco reflejante
|
|
'harness', -- Arnés de seguridad
|
|
'lifeline', -- Línea de vida
|
|
'rain_suit', -- Impermeable
|
|
'coveralls', -- Overol
|
|
'welding_jacket', -- Chaqueta de soldadura
|
|
'other' -- Otro
|
|
);
|
|
|
|
COMMENT ON TYPE safety_management.epp_type IS 'Tipos de Equipo de Protección Personal según NOM-017-STPS-2008';
|
|
```
|
|
|
|
### 2. epp_status (Estados de EPP)
|
|
```sql
|
|
CREATE TYPE safety_management.epp_status AS ENUM (
|
|
'available', -- Disponible en almacén
|
|
'assigned', -- Asignado a trabajador
|
|
'in_use', -- En uso activo
|
|
'expired', -- Vida útil vencida
|
|
'damaged', -- Dañado o defectuoso
|
|
'lost', -- Extraviado
|
|
'retired' -- Dado de baja
|
|
);
|
|
|
|
COMMENT ON TYPE safety_management.epp_status IS 'Estados del ciclo de vida del EPP';
|
|
```
|
|
|
|
### 3. risk_level (Nivel de Riesgo)
|
|
```sql
|
|
CREATE TYPE safety_management.risk_level AS ENUM (
|
|
'low', -- Riesgo bajo
|
|
'medium', -- Riesgo medio
|
|
'high', -- Riesgo alto
|
|
'critical' -- Riesgo crítico - requiere acción inmediata
|
|
);
|
|
|
|
COMMENT ON TYPE safety_management.risk_level IS 'Niveles de riesgo para hallazgos e incidentes';
|
|
```
|
|
|
|
### 4. incident_severity (Severidad de Incidente)
|
|
```sql
|
|
CREATE TYPE safety_management.incident_severity AS ENUM (
|
|
'near_miss', -- Casi accidente (sin lesiones)
|
|
'first_aid', -- Requiere primeros auxilios
|
|
'minor', -- Lesión menor sin días perdidos
|
|
'lost_time', -- Lesión con días perdidos
|
|
'serious', -- Lesión grave con hospitalización
|
|
'fatal', -- Accidente fatal
|
|
'property_damage' -- Daño a propiedad sin lesiones
|
|
);
|
|
|
|
COMMENT ON TYPE safety_management.incident_severity IS 'Clasificación de gravedad de incidentes laborales';
|
|
```
|
|
|
|
### 5. incident_status (Estado de Incidente)
|
|
```sql
|
|
CREATE TYPE safety_management.incident_status AS ENUM (
|
|
'reported', -- Recién reportado
|
|
'under_investigation', -- En investigación
|
|
'investigation_completed', -- Investigación completa
|
|
'actions_pending', -- Acciones correctivas pendientes
|
|
'closed', -- Cerrado con medidas implementadas
|
|
'archived' -- Archivado
|
|
);
|
|
|
|
COMMENT ON TYPE safety_management.incident_status IS 'Estados del ciclo de vida de un incidente';
|
|
```
|
|
|
|
### 6. action_status (Estado de Acción Correctiva)
|
|
```sql
|
|
CREATE TYPE safety_management.action_status AS ENUM (
|
|
'pending', -- Pendiente de iniciar
|
|
'in_progress', -- En progreso
|
|
'completed', -- Completada
|
|
'verified', -- Verificada y aprobada
|
|
'cancelled', -- Cancelada
|
|
'overdue' -- Vencida
|
|
);
|
|
|
|
COMMENT ON TYPE safety_management.action_status IS 'Estados de acciones correctivas y preventivas';
|
|
```
|
|
|
|
### 7. inspection_status (Estado de Inspección)
|
|
```sql
|
|
CREATE TYPE safety_management.inspection_status AS ENUM (
|
|
'scheduled', -- Programada
|
|
'in_progress', -- En ejecución
|
|
'completed', -- Completada
|
|
'cancelled', -- Cancelada
|
|
'overdue' -- Vencida sin realizar
|
|
);
|
|
|
|
COMMENT ON TYPE safety_management.inspection_status IS 'Estados de inspecciones de seguridad';
|
|
```
|
|
|
|
### 8. training_status (Estado de Capacitación)
|
|
```sql
|
|
CREATE TYPE safety_management.training_status AS ENUM (
|
|
'scheduled', -- Programada
|
|
'in_progress', -- En curso
|
|
'completed', -- Completada
|
|
'cancelled', -- Cancelada
|
|
'postponed' -- Pospuesta
|
|
);
|
|
|
|
COMMENT ON TYPE safety_management.training_status IS 'Estados de sesiones de capacitación';
|
|
```
|
|
|
|
---
|
|
|
|
## Tablas
|
|
|
|
### 1. epp_catalog (Catálogo de EPP)
|
|
|
|
**Descripción:** Catálogo maestro de tipos de EPP con especificaciones técnicas y normativas aplicables.
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Schema: safety_management
|
|
-- Tabla: epp_catalog
|
|
-- Descripción: Catálogo de tipos de EPP con especificaciones técnicas
|
|
-- Módulo: MAI-007
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS safety_management.epp_catalog (
|
|
-- Primary Key
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
|
|
-- Multi-tenant
|
|
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Datos del EPP
|
|
code VARCHAR(50) NOT NULL,
|
|
name VARCHAR(200) NOT NULL,
|
|
epp_type safety_management.epp_type NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Especificaciones técnicas
|
|
manufacturer VARCHAR(200),
|
|
model VARCHAR(100),
|
|
technical_specs JSONB DEFAULT '{}',
|
|
|
|
-- Normativas
|
|
norm_references TEXT[], -- ['NOM-017-STPS-2008', 'ANSI Z87.1']
|
|
certification_required BOOLEAN NOT NULL DEFAULT false,
|
|
certification_details JSONB DEFAULT '{}',
|
|
|
|
-- Vida útil
|
|
useful_life_days INTEGER NOT NULL DEFAULT 365, -- Días de vida útil
|
|
replacement_alert_days INTEGER NOT NULL DEFAULT 30, -- Alertar N días antes
|
|
|
|
-- Inventario
|
|
minimum_stock INTEGER NOT NULL DEFAULT 0,
|
|
unit_cost DECIMAL(12, 2),
|
|
|
|
-- Metadata
|
|
image_url TEXT,
|
|
care_instructions TEXT,
|
|
usage_guidelines TEXT,
|
|
metadata JSONB DEFAULT '{}',
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_by UUID REFERENCES core_users.users(id),
|
|
updated_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Soft delete
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Constraints
|
|
CONSTRAINT uq_epp_catalog_code_tenant UNIQUE (tenant_id, code),
|
|
CONSTRAINT chk_epp_catalog_useful_life CHECK (useful_life_days > 0),
|
|
CONSTRAINT chk_epp_catalog_alert_days CHECK (replacement_alert_days >= 0 AND replacement_alert_days < useful_life_days)
|
|
);
|
|
|
|
-- Comentarios
|
|
COMMENT ON TABLE safety_management.epp_catalog IS 'Catálogo maestro de tipos de EPP con especificaciones técnicas';
|
|
COMMENT ON COLUMN safety_management.epp_catalog.code IS 'Código único del EPP por tenant';
|
|
COMMENT ON COLUMN safety_management.epp_catalog.epp_type IS 'Tipo de EPP según enumeración';
|
|
COMMENT ON COLUMN safety_management.epp_catalog.norm_references IS 'Normativas aplicables (NOM-STPS, ANSI, etc.)';
|
|
COMMENT ON COLUMN safety_management.epp_catalog.useful_life_days IS 'Vida útil en días desde asignación';
|
|
COMMENT ON COLUMN safety_management.epp_catalog.replacement_alert_days IS 'Días antes de vencimiento para alertar';
|
|
|
|
-- ============================================================================
|
|
-- ÍNDICES
|
|
-- ============================================================================
|
|
|
|
-- Índice obligatorio para RLS
|
|
CREATE INDEX idx_epp_catalog_tenant_id ON safety_management.epp_catalog(tenant_id);
|
|
|
|
-- Índices de búsqueda
|
|
CREATE INDEX idx_epp_catalog_code ON safety_management.epp_catalog(code);
|
|
CREATE INDEX idx_epp_catalog_type ON safety_management.epp_catalog(epp_type);
|
|
CREATE INDEX idx_epp_catalog_active ON safety_management.epp_catalog(is_active) WHERE is_active = true;
|
|
|
|
-- Índice para búsqueda de texto
|
|
CREATE INDEX idx_epp_catalog_name_trgm ON safety_management.epp_catalog
|
|
USING gin (name gin_trgm_ops);
|
|
|
|
-- ============================================================================
|
|
-- RLS POLICIES
|
|
-- ============================================================================
|
|
|
|
ALTER TABLE safety_management.epp_catalog ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_select ON safety_management.epp_catalog
|
|
FOR SELECT
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_insert ON safety_management.epp_catalog
|
|
FOR INSERT
|
|
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_update ON safety_management.epp_catalog
|
|
FOR UPDATE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_delete ON safety_management.epp_catalog
|
|
FOR DELETE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- ============================================================================
|
|
-- TRIGGERS
|
|
-- ============================================================================
|
|
|
|
-- Trigger para updated_at
|
|
CREATE TRIGGER trg_epp_catalog_update_timestamp
|
|
BEFORE UPDATE ON safety_management.epp_catalog
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION core_shared.set_updated_at();
|
|
```
|
|
|
|
---
|
|
|
|
### 2. epp_assignments (Asignaciones de EPP)
|
|
|
|
**Descripción:** Registro de asignación de EPP a trabajadores con control de vida útil y trazabilidad.
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Schema: safety_management
|
|
-- Tabla: epp_assignments
|
|
-- Descripción: Asignaciones de EPP a trabajadores con control de vigencia
|
|
-- Módulo: MAI-007
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS safety_management.epp_assignments (
|
|
-- Primary Key
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
|
|
-- Multi-tenant
|
|
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Relaciones
|
|
epp_catalog_id UUID NOT NULL REFERENCES safety_management.epp_catalog(id),
|
|
worker_id UUID NOT NULL, -- FK a tabla de trabajadores de MAI-001
|
|
project_id UUID, -- FK a tabla de proyectos de MAI-002 (opcional)
|
|
|
|
-- Datos de la asignación
|
|
assignment_number VARCHAR(50) NOT NULL, -- Número de acta de entrega
|
|
assignment_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
|
quantity INTEGER NOT NULL DEFAULT 1,
|
|
|
|
-- Control de vida útil
|
|
expiry_date DATE NOT NULL,
|
|
replaced_on DATE,
|
|
replacement_reason TEXT,
|
|
|
|
-- Estado
|
|
status safety_management.epp_status NOT NULL DEFAULT 'assigned',
|
|
|
|
-- Entrega y recepción
|
|
delivery_notes TEXT,
|
|
delivery_signature_url TEXT, -- Firma digital del trabajador
|
|
delivery_photo_url TEXT, -- Foto de evidencia
|
|
delivered_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Devolución
|
|
return_date DATE,
|
|
return_condition TEXT,
|
|
return_notes TEXT,
|
|
returned_to UUID REFERENCES core_users.users(id),
|
|
|
|
-- Costos
|
|
unit_cost DECIMAL(12, 2),
|
|
total_cost DECIMAL(12, 2) GENERATED ALWAYS AS (quantity * unit_cost) STORED,
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}',
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_by UUID REFERENCES core_users.users(id),
|
|
updated_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Soft delete
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Constraints
|
|
CONSTRAINT uq_epp_assignment_number_tenant UNIQUE (tenant_id, assignment_number),
|
|
CONSTRAINT chk_epp_assignment_quantity CHECK (quantity > 0),
|
|
CONSTRAINT chk_epp_assignment_expiry CHECK (expiry_date >= assignment_date),
|
|
CONSTRAINT chk_epp_assignment_return CHECK (return_date IS NULL OR return_date >= assignment_date)
|
|
);
|
|
|
|
-- Comentarios
|
|
COMMENT ON TABLE safety_management.epp_assignments IS 'Asignaciones de EPP a trabajadores con control de vigencia';
|
|
COMMENT ON COLUMN safety_management.epp_assignments.assignment_number IS 'Número de acta de entrega único por tenant';
|
|
COMMENT ON COLUMN safety_management.epp_assignments.expiry_date IS 'Fecha de vencimiento calculada según vida útil';
|
|
COMMENT ON COLUMN safety_management.epp_assignments.delivery_signature_url IS 'URL de firma digital del trabajador en S3';
|
|
|
|
-- ============================================================================
|
|
-- ÍNDICES
|
|
-- ============================================================================
|
|
|
|
-- Índice obligatorio para RLS
|
|
CREATE INDEX idx_epp_assignments_tenant_id ON safety_management.epp_assignments(tenant_id);
|
|
|
|
-- Índices de relaciones
|
|
CREATE INDEX idx_epp_assignments_epp_catalog ON safety_management.epp_assignments(epp_catalog_id);
|
|
CREATE INDEX idx_epp_assignments_worker ON safety_management.epp_assignments(worker_id);
|
|
CREATE INDEX idx_epp_assignments_project ON safety_management.epp_assignments(project_id);
|
|
|
|
-- Índices de búsqueda y filtros
|
|
CREATE INDEX idx_epp_assignments_status ON safety_management.epp_assignments(status);
|
|
CREATE INDEX idx_epp_assignments_expiry ON safety_management.epp_assignments(expiry_date)
|
|
WHERE status IN ('assigned', 'in_use');
|
|
CREATE INDEX idx_epp_assignments_assignment_date ON safety_management.epp_assignments(assignment_date DESC);
|
|
|
|
-- Índice compuesto para alertas de vencimiento
|
|
CREATE INDEX idx_epp_assignments_alerts ON safety_management.epp_assignments(tenant_id, expiry_date, status)
|
|
WHERE status IN ('assigned', 'in_use') AND is_active = true;
|
|
|
|
-- ============================================================================
|
|
-- RLS POLICIES
|
|
-- ============================================================================
|
|
|
|
ALTER TABLE safety_management.epp_assignments ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_select ON safety_management.epp_assignments
|
|
FOR SELECT
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_insert ON safety_management.epp_assignments
|
|
FOR INSERT
|
|
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_update ON safety_management.epp_assignments
|
|
FOR UPDATE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_delete ON safety_management.epp_assignments
|
|
FOR DELETE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- ============================================================================
|
|
-- TRIGGERS
|
|
-- ============================================================================
|
|
|
|
-- Trigger para updated_at
|
|
CREATE TRIGGER trg_epp_assignments_update_timestamp
|
|
BEFORE UPDATE ON safety_management.epp_assignments
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION core_shared.set_updated_at();
|
|
|
|
-- Trigger para calcular expiry_date automáticamente
|
|
CREATE TRIGGER trg_epp_assignments_calculate_expiry
|
|
BEFORE INSERT OR UPDATE ON safety_management.epp_assignments
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION safety_management.calculate_epp_expiry_date();
|
|
```
|
|
|
|
---
|
|
|
|
### 3. safety_inspections (Inspecciones de Seguridad)
|
|
|
|
**Descripción:** Registro de inspecciones de seguridad realizadas en obra con checklist y hallazgos.
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Schema: safety_management
|
|
-- Tabla: safety_inspections
|
|
-- Descripción: Inspecciones de seguridad con geolocalización y evidencias
|
|
-- Módulo: MAI-007
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS safety_management.safety_inspections (
|
|
-- Primary Key
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
|
|
-- Multi-tenant
|
|
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Relaciones
|
|
project_id UUID NOT NULL, -- FK a tabla de proyectos de MAI-002
|
|
template_id UUID, -- FK a tabla de plantillas (safety.inspection_templates)
|
|
|
|
-- Datos de la inspección
|
|
inspection_number VARCHAR(50) NOT NULL,
|
|
inspection_type VARCHAR(100) NOT NULL, -- 'Diaria', 'Semanal', 'Mensual', 'Especial'
|
|
inspection_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
|
inspection_time TIME NOT NULL DEFAULT CURRENT_TIME,
|
|
|
|
-- Ubicación
|
|
location_description TEXT,
|
|
gps_latitude DECIMAL(10, 8),
|
|
gps_longitude DECIMAL(11, 8),
|
|
gps_accuracy DECIMAL(8, 2), -- Precisión en metros
|
|
|
|
-- Inspector
|
|
inspector_id UUID NOT NULL REFERENCES core_users.users(id),
|
|
inspector_name VARCHAR(200),
|
|
inspector_position VARCHAR(100),
|
|
|
|
-- Resultados
|
|
status safety_management.inspection_status NOT NULL DEFAULT 'scheduled',
|
|
total_items_checked INTEGER NOT NULL DEFAULT 0,
|
|
conforming_items INTEGER NOT NULL DEFAULT 0,
|
|
non_conforming_items INTEGER NOT NULL DEFAULT 0,
|
|
not_applicable_items INTEGER NOT NULL DEFAULT 0,
|
|
overall_score DECIMAL(5, 2), -- Porcentaje de cumplimiento
|
|
|
|
-- Clasificación de hallazgos
|
|
critical_findings INTEGER NOT NULL DEFAULT 0,
|
|
high_risk_findings INTEGER NOT NULL DEFAULT 0,
|
|
medium_risk_findings INTEGER NOT NULL DEFAULT 0,
|
|
low_risk_findings INTEGER NOT NULL DEFAULT 0,
|
|
|
|
-- Datos del checklist
|
|
checklist_data JSONB NOT NULL DEFAULT '[]', -- Array de items con resultados
|
|
|
|
-- Observaciones
|
|
general_observations TEXT,
|
|
recommendations TEXT,
|
|
immediate_actions_taken TEXT,
|
|
|
|
-- Evidencias
|
|
photo_urls TEXT[], -- Array de URLs de fotos en S3
|
|
document_urls TEXT[],
|
|
|
|
-- Firma y aprobación
|
|
inspector_signature_url TEXT,
|
|
reviewed_by UUID REFERENCES core_users.users(id),
|
|
reviewed_at TIMESTAMPTZ,
|
|
review_notes TEXT,
|
|
|
|
-- Seguimiento
|
|
requires_followup BOOLEAN NOT NULL DEFAULT false,
|
|
followup_date DATE,
|
|
followup_completed BOOLEAN NOT NULL DEFAULT false,
|
|
|
|
-- Metadata
|
|
weather_conditions VARCHAR(100),
|
|
workers_present INTEGER,
|
|
metadata JSONB DEFAULT '{}',
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_by UUID REFERENCES core_users.users(id),
|
|
updated_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Soft delete
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Constraints
|
|
CONSTRAINT uq_safety_inspection_number_tenant UNIQUE (tenant_id, inspection_number),
|
|
CONSTRAINT chk_safety_inspection_items CHECK (
|
|
total_items_checked = (conforming_items + non_conforming_items + not_applicable_items)
|
|
),
|
|
CONSTRAINT chk_safety_inspection_findings CHECK (
|
|
non_conforming_items = (critical_findings + high_risk_findings + medium_risk_findings + low_risk_findings)
|
|
),
|
|
CONSTRAINT chk_safety_inspection_score CHECK (overall_score >= 0 AND overall_score <= 100)
|
|
);
|
|
|
|
-- Comentarios
|
|
COMMENT ON TABLE safety_management.safety_inspections IS 'Inspecciones de seguridad con geolocalización y evidencias fotográficas';
|
|
COMMENT ON COLUMN safety_management.safety_inspections.checklist_data IS 'Array JSONB con items del checklist y sus resultados';
|
|
COMMENT ON COLUMN safety_management.safety_inspections.gps_latitude IS 'Latitud GPS capturada en app móvil';
|
|
COMMENT ON COLUMN safety_management.safety_inspections.overall_score IS 'Porcentaje de cumplimiento (conforming / total * 100)';
|
|
|
|
-- ============================================================================
|
|
-- ÍNDICES
|
|
-- ============================================================================
|
|
|
|
-- Índice obligatorio para RLS
|
|
CREATE INDEX idx_safety_inspections_tenant_id ON safety_management.safety_inspections(tenant_id);
|
|
|
|
-- Índices de relaciones
|
|
CREATE INDEX idx_safety_inspections_project ON safety_management.safety_inspections(project_id);
|
|
CREATE INDEX idx_safety_inspections_inspector ON safety_management.safety_inspections(inspector_id);
|
|
CREATE INDEX idx_safety_inspections_template ON safety_management.safety_inspections(template_id);
|
|
|
|
-- Índices de búsqueda
|
|
CREATE INDEX idx_safety_inspections_status ON safety_management.safety_inspections(status);
|
|
CREATE INDEX idx_safety_inspections_date ON safety_management.safety_inspections(inspection_date DESC);
|
|
CREATE INDEX idx_safety_inspections_type ON safety_management.safety_inspections(inspection_type);
|
|
|
|
-- Índice para inspecciones con hallazgos críticos
|
|
CREATE INDEX idx_safety_inspections_critical ON safety_management.safety_inspections(tenant_id, project_id, critical_findings)
|
|
WHERE critical_findings > 0 AND is_active = true;
|
|
|
|
-- Índice para seguimiento
|
|
CREATE INDEX idx_safety_inspections_followup ON safety_management.safety_inspections(followup_date)
|
|
WHERE requires_followup = true AND followup_completed = false;
|
|
|
|
-- Índice GIN para búsqueda en checklist_data
|
|
CREATE INDEX idx_safety_inspections_checklist ON safety_management.safety_inspections
|
|
USING gin (checklist_data jsonb_path_ops);
|
|
|
|
-- ============================================================================
|
|
-- RLS POLICIES
|
|
-- ============================================================================
|
|
|
|
ALTER TABLE safety_management.safety_inspections ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_select ON safety_management.safety_inspections
|
|
FOR SELECT
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_insert ON safety_management.safety_inspections
|
|
FOR INSERT
|
|
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_update ON safety_management.safety_inspections
|
|
FOR UPDATE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_delete ON safety_management.safety_inspections
|
|
FOR DELETE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- ============================================================================
|
|
-- TRIGGERS
|
|
-- ============================================================================
|
|
|
|
-- Trigger para updated_at
|
|
CREATE TRIGGER trg_safety_inspections_update_timestamp
|
|
BEFORE UPDATE ON safety_management.safety_inspections
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION core_shared.set_updated_at();
|
|
|
|
-- Trigger para calcular overall_score automáticamente
|
|
CREATE TRIGGER trg_safety_inspections_calculate_score
|
|
BEFORE INSERT OR UPDATE ON safety_management.safety_inspections
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION safety_management.calculate_inspection_score();
|
|
```
|
|
|
|
---
|
|
|
|
### 4. incidents (Reportes de Incidentes)
|
|
|
|
**Descripción:** Registro de incidentes y accidentes laborales con geolocalización y evidencias.
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Schema: safety_management
|
|
-- Tabla: incidents
|
|
-- Descripción: Registro de incidentes y accidentes laborales
|
|
-- Módulo: MAI-007
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS safety_management.incidents (
|
|
-- Primary Key
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
|
|
-- Multi-tenant
|
|
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Relaciones
|
|
project_id UUID NOT NULL, -- FK a tabla de proyectos de MAI-002
|
|
|
|
-- Identificación del incidente
|
|
incident_number VARCHAR(50) NOT NULL,
|
|
incident_date DATE NOT NULL,
|
|
incident_time TIME NOT NULL,
|
|
reported_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
|
|
-- Clasificación
|
|
severity safety_management.incident_severity NOT NULL,
|
|
status safety_management.incident_status NOT NULL DEFAULT 'reported',
|
|
incident_type VARCHAR(100) NOT NULL, -- 'Caída', 'Golpe', 'Corte', etc.
|
|
|
|
-- Ubicación
|
|
location_description TEXT NOT NULL,
|
|
area VARCHAR(200),
|
|
gps_latitude DECIMAL(10, 8),
|
|
gps_longitude DECIMAL(11, 8),
|
|
gps_accuracy DECIMAL(8, 2),
|
|
|
|
-- Descripción del incidente
|
|
description TEXT NOT NULL,
|
|
immediate_cause TEXT,
|
|
activity_at_time TEXT,
|
|
weather_conditions VARCHAR(100),
|
|
|
|
-- Personas involucradas
|
|
affected_worker_id UUID, -- FK a tabla de trabajadores
|
|
affected_worker_name VARCHAR(200),
|
|
affected_worker_position VARCHAR(100),
|
|
affected_worker_age INTEGER,
|
|
affected_worker_experience_months INTEGER,
|
|
|
|
-- Lesiones
|
|
injury_type VARCHAR(200),
|
|
injury_description TEXT,
|
|
body_part_affected VARCHAR(200),
|
|
medical_attention_required BOOLEAN NOT NULL DEFAULT false,
|
|
hospitalization_required BOOLEAN NOT NULL DEFAULT false,
|
|
emergency_services_called BOOLEAN NOT NULL DEFAULT false,
|
|
|
|
-- Impacto laboral
|
|
work_days_lost INTEGER DEFAULT 0,
|
|
restricted_work_days INTEGER DEFAULT 0,
|
|
estimated_return_date DATE,
|
|
|
|
-- EPP
|
|
epp_being_used TEXT[],
|
|
epp_condition TEXT,
|
|
epp_adequate_for_task BOOLEAN,
|
|
|
|
-- Reportado por
|
|
reported_by_id UUID NOT NULL REFERENCES core_users.users(id),
|
|
reported_by_name VARCHAR(200),
|
|
reporter_position VARCHAR(100),
|
|
witness_names TEXT[],
|
|
|
|
-- Evidencias
|
|
photo_urls TEXT[],
|
|
document_urls TEXT[],
|
|
video_urls TEXT[],
|
|
|
|
-- Notificaciones
|
|
authorities_notified BOOLEAN NOT NULL DEFAULT false,
|
|
authorities_notified_at TIMESTAMPTZ,
|
|
authority_names TEXT[],
|
|
family_notified BOOLEAN NOT NULL DEFAULT false,
|
|
|
|
-- Costos (registrados posteriormente)
|
|
medical_costs DECIMAL(12, 2) DEFAULT 0,
|
|
legal_costs DECIMAL(12, 2) DEFAULT 0,
|
|
equipment_damage_costs DECIMAL(12, 2) DEFAULT 0,
|
|
productivity_loss_costs DECIMAL(12, 2) DEFAULT 0,
|
|
total_costs DECIMAL(12, 2) GENERATED ALWAYS AS (
|
|
COALESCE(medical_costs, 0) +
|
|
COALESCE(legal_costs, 0) +
|
|
COALESCE(equipment_damage_costs, 0) +
|
|
COALESCE(productivity_loss_costs, 0)
|
|
) STORED,
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}',
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_by UUID REFERENCES core_users.users(id),
|
|
updated_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Soft delete
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Constraints
|
|
CONSTRAINT uq_incident_number_tenant UNIQUE (tenant_id, incident_number),
|
|
CONSTRAINT chk_incident_work_days CHECK (work_days_lost >= 0),
|
|
CONSTRAINT chk_incident_costs CHECK (
|
|
medical_costs >= 0 AND
|
|
legal_costs >= 0 AND
|
|
equipment_damage_costs >= 0 AND
|
|
productivity_loss_costs >= 0
|
|
)
|
|
);
|
|
|
|
-- Comentarios
|
|
COMMENT ON TABLE safety_management.incidents IS 'Registro de incidentes y accidentes laborales con geolocalización';
|
|
COMMENT ON COLUMN safety_management.incidents.severity IS 'Gravedad del incidente según clasificación';
|
|
COMMENT ON COLUMN safety_management.incidents.work_days_lost IS 'Días laborales perdidos por el trabajador';
|
|
COMMENT ON COLUMN safety_management.incidents.total_costs IS 'Costo total calculado automáticamente';
|
|
|
|
-- ============================================================================
|
|
-- ÍNDICES
|
|
-- ============================================================================
|
|
|
|
-- Índice obligatorio para RLS
|
|
CREATE INDEX idx_incidents_tenant_id ON safety_management.incidents(tenant_id);
|
|
|
|
-- Índices de relaciones
|
|
CREATE INDEX idx_incidents_project ON safety_management.incidents(project_id);
|
|
CREATE INDEX idx_incidents_worker ON safety_management.incidents(affected_worker_id);
|
|
CREATE INDEX idx_incidents_reporter ON safety_management.incidents(reported_by_id);
|
|
|
|
-- Índices de búsqueda y análisis
|
|
CREATE INDEX idx_incidents_severity ON safety_management.incidents(severity);
|
|
CREATE INDEX idx_incidents_status ON safety_management.incidents(status);
|
|
CREATE INDEX idx_incidents_date ON safety_management.incidents(incident_date DESC);
|
|
CREATE INDEX idx_incidents_type ON safety_management.incidents(incident_type);
|
|
|
|
-- Índice para KPIs de seguridad
|
|
CREATE INDEX idx_incidents_kpi ON safety_management.incidents(tenant_id, incident_date, severity)
|
|
WHERE is_active = true;
|
|
|
|
-- Índice para incidentes que requieren seguimiento
|
|
CREATE INDEX idx_incidents_pending ON safety_management.incidents(status)
|
|
WHERE status IN ('reported', 'under_investigation', 'actions_pending');
|
|
|
|
-- Índice para incidentes graves
|
|
CREATE INDEX idx_incidents_serious ON safety_management.incidents(tenant_id, project_id, severity)
|
|
WHERE severity IN ('serious', 'fatal') AND is_active = true;
|
|
|
|
-- ============================================================================
|
|
-- RLS POLICIES
|
|
-- ============================================================================
|
|
|
|
ALTER TABLE safety_management.incidents ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_select ON safety_management.incidents
|
|
FOR SELECT
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_insert ON safety_management.incidents
|
|
FOR INSERT
|
|
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_update ON safety_management.incidents
|
|
FOR UPDATE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_delete ON safety_management.incidents
|
|
FOR DELETE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- ============================================================================
|
|
-- TRIGGERS
|
|
-- ============================================================================
|
|
|
|
-- Trigger para updated_at
|
|
CREATE TRIGGER trg_incidents_update_timestamp
|
|
BEFORE UPDATE ON safety_management.incidents
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION core_shared.set_updated_at();
|
|
|
|
-- Trigger para notificaciones automáticas de incidentes graves
|
|
CREATE TRIGGER trg_incidents_notify_serious
|
|
AFTER INSERT OR UPDATE ON safety_management.incidents
|
|
FOR EACH ROW
|
|
WHEN (NEW.severity IN ('serious', 'fatal'))
|
|
EXECUTE FUNCTION safety_management.notify_serious_incident();
|
|
```
|
|
|
|
---
|
|
|
|
### 5. incident_investigations (Investigaciones de Incidentes)
|
|
|
|
**Descripción:** Investigación de causas raíz de incidentes con análisis de Ishikawa y acciones correctivas.
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Schema: safety_management
|
|
-- Tabla: incident_investigations
|
|
-- Descripción: Investigación de causas raíz de incidentes
|
|
-- Módulo: MAI-007
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS safety_management.incident_investigations (
|
|
-- Primary Key
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
|
|
-- Multi-tenant
|
|
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Relación
|
|
incident_id UUID NOT NULL REFERENCES safety_management.incidents(id) ON DELETE CASCADE,
|
|
|
|
-- Investigación
|
|
investigation_number VARCHAR(50) NOT NULL,
|
|
investigation_start_date DATE NOT NULL,
|
|
investigation_end_date DATE,
|
|
|
|
-- Equipo investigador
|
|
lead_investigator_id UUID NOT NULL REFERENCES core_users.users(id),
|
|
team_members UUID[], -- Array de IDs de usuarios
|
|
team_members_names TEXT[],
|
|
|
|
-- Metodología
|
|
investigation_method VARCHAR(100) DEFAULT 'Ishikawa', -- 'Ishikawa', '5 Whys', 'Root Cause Tree'
|
|
|
|
-- Análisis de causas (Ishikawa - Espina de Pescado)
|
|
people_factors TEXT[], -- Factores humanos
|
|
method_factors TEXT[], -- Factores de método/procedimiento
|
|
machine_factors TEXT[], -- Factores de equipo/maquinaria
|
|
material_factors TEXT[], -- Factores de materiales
|
|
environment_factors TEXT[], -- Factores ambientales
|
|
management_factors TEXT[], -- Factores de gestión
|
|
|
|
-- Causa raíz identificada
|
|
root_cause TEXT,
|
|
contributing_factors TEXT[],
|
|
|
|
-- Hallazgos
|
|
findings TEXT NOT NULL,
|
|
evidence_collected TEXT[],
|
|
witness_statements JSONB DEFAULT '[]', -- Array de testimonios
|
|
|
|
-- Conclusiones
|
|
conclusions TEXT,
|
|
preventability_assessment TEXT,
|
|
similar_incidents_reviewed INTEGER DEFAULT 0,
|
|
|
|
-- Recomendaciones
|
|
immediate_actions_recommended TEXT[],
|
|
corrective_actions_recommended TEXT[],
|
|
preventive_actions_recommended TEXT[],
|
|
|
|
-- Aprobación
|
|
approved_by UUID REFERENCES core_users.users(id),
|
|
approved_at TIMESTAMPTZ,
|
|
approval_notes TEXT,
|
|
|
|
-- Documentos
|
|
report_url TEXT, -- URL del informe oficial en S3
|
|
document_urls TEXT[],
|
|
photo_urls TEXT[],
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}',
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_by UUID REFERENCES core_users.users(id),
|
|
updated_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Soft delete
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Constraints
|
|
CONSTRAINT uq_investigation_number_tenant UNIQUE (tenant_id, investigation_number),
|
|
CONSTRAINT uq_investigation_incident UNIQUE (incident_id), -- Una investigación por incidente
|
|
CONSTRAINT chk_investigation_dates CHECK (
|
|
investigation_end_date IS NULL OR investigation_end_date >= investigation_start_date
|
|
)
|
|
);
|
|
|
|
-- Comentarios
|
|
COMMENT ON TABLE safety_management.incident_investigations IS 'Investigación de causas raíz de incidentes con metodología Ishikawa';
|
|
COMMENT ON COLUMN safety_management.incident_investigations.people_factors IS 'Factores humanos en diagrama de Ishikawa';
|
|
COMMENT ON COLUMN safety_management.incident_investigations.root_cause IS 'Causa raíz identificada del incidente';
|
|
|
|
-- ============================================================================
|
|
-- ÍNDICES
|
|
-- ============================================================================
|
|
|
|
-- Índice obligatorio para RLS
|
|
CREATE INDEX idx_incident_investigations_tenant_id ON safety_management.incident_investigations(tenant_id);
|
|
|
|
-- Índices de relaciones
|
|
CREATE INDEX idx_incident_investigations_incident ON safety_management.incident_investigations(incident_id);
|
|
CREATE INDEX idx_incident_investigations_lead ON safety_management.incident_investigations(lead_investigator_id);
|
|
|
|
-- Índices de búsqueda
|
|
CREATE INDEX idx_incident_investigations_dates ON safety_management.incident_investigations(investigation_start_date DESC);
|
|
CREATE INDEX idx_incident_investigations_pending ON safety_management.incident_investigations(investigation_end_date)
|
|
WHERE investigation_end_date IS NULL AND is_active = true;
|
|
|
|
-- Índice GIN para búsqueda en testimonios
|
|
CREATE INDEX idx_incident_investigations_witnesses ON safety_management.incident_investigations
|
|
USING gin (witness_statements jsonb_path_ops);
|
|
|
|
-- ============================================================================
|
|
-- RLS POLICIES
|
|
-- ============================================================================
|
|
|
|
ALTER TABLE safety_management.incident_investigations ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_select ON safety_management.incident_investigations
|
|
FOR SELECT
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_insert ON safety_management.incident_investigations
|
|
FOR INSERT
|
|
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_update ON safety_management.incident_investigations
|
|
FOR UPDATE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_delete ON safety_management.incident_investigations
|
|
FOR DELETE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- ============================================================================
|
|
-- TRIGGERS
|
|
-- ============================================================================
|
|
|
|
-- Trigger para updated_at
|
|
CREATE TRIGGER trg_incident_investigations_update_timestamp
|
|
BEFORE UPDATE ON safety_management.incident_investigations
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION core_shared.set_updated_at();
|
|
|
|
-- Trigger para actualizar estado del incidente al completar investigación
|
|
CREATE TRIGGER trg_incident_investigations_complete
|
|
AFTER UPDATE ON safety_management.incident_investigations
|
|
FOR EACH ROW
|
|
WHEN (NEW.investigation_end_date IS NOT NULL AND OLD.investigation_end_date IS NULL)
|
|
EXECUTE FUNCTION safety_management.update_incident_status_investigated();
|
|
```
|
|
|
|
---
|
|
|
|
### 6. trainings (Capacitaciones de Seguridad)
|
|
|
|
**Descripción:** Catálogo de capacitaciones de seguridad y programación de sesiones.
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Schema: safety_management
|
|
-- Tabla: trainings
|
|
-- Descripción: Sesiones de capacitación de seguridad programadas
|
|
-- Módulo: MAI-007
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS safety_management.trainings (
|
|
-- Primary Key
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
|
|
-- Multi-tenant
|
|
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Relación
|
|
project_id UUID, -- FK a tabla de proyectos (opcional, puede ser corporativa)
|
|
|
|
-- Datos de la capacitación
|
|
training_code VARCHAR(50) NOT NULL,
|
|
training_name VARCHAR(200) NOT NULL,
|
|
training_type VARCHAR(100) NOT NULL, -- 'Inducción', 'Trabajo en Altura', 'EPP', etc.
|
|
description TEXT,
|
|
|
|
-- Normativa
|
|
norm_references TEXT[], -- Normativas que respaldan la capacitación
|
|
is_mandatory BOOLEAN NOT NULL DEFAULT false,
|
|
required_for_positions TEXT[], -- Puestos que requieren esta capacitación
|
|
|
|
-- Programación
|
|
scheduled_date DATE NOT NULL,
|
|
scheduled_start_time TIME NOT NULL,
|
|
scheduled_end_time TIME NOT NULL,
|
|
duration_hours DECIMAL(4, 2),
|
|
|
|
-- Ubicación
|
|
location VARCHAR(200),
|
|
location_type VARCHAR(50) DEFAULT 'presencial', -- 'presencial', 'online', 'híbrida'
|
|
meeting_url TEXT, -- Para capacitaciones online
|
|
|
|
-- Instructor
|
|
instructor_id UUID REFERENCES core_users.users(id),
|
|
instructor_name VARCHAR(200),
|
|
instructor_credentials TEXT,
|
|
external_instructor BOOLEAN NOT NULL DEFAULT false,
|
|
instructor_company VARCHAR(200),
|
|
|
|
-- Capacidad
|
|
max_participants INTEGER,
|
|
min_participants INTEGER,
|
|
|
|
-- Estado
|
|
status safety_management.training_status NOT NULL DEFAULT 'scheduled',
|
|
|
|
-- Contenido
|
|
topics TEXT[],
|
|
learning_objectives TEXT[],
|
|
materials_provided TEXT[],
|
|
materials_urls TEXT[], -- URLs de materiales en S3
|
|
|
|
-- Evaluación
|
|
has_evaluation BOOLEAN NOT NULL DEFAULT false,
|
|
evaluation_type VARCHAR(50), -- 'test', 'práctica', 'ambos'
|
|
passing_score DECIMAL(5, 2), -- Calificación mínima aprobatoria
|
|
|
|
-- Certificación
|
|
issues_certificate BOOLEAN NOT NULL DEFAULT true,
|
|
certificate_template_url TEXT,
|
|
certificate_validity_months INTEGER, -- Meses de validez del certificado
|
|
|
|
-- Asistencia
|
|
registered_participants INTEGER DEFAULT 0,
|
|
actual_attendees INTEGER DEFAULT 0,
|
|
|
|
-- Resultados
|
|
average_score DECIMAL(5, 2),
|
|
passing_participants INTEGER DEFAULT 0,
|
|
failing_participants INTEGER DEFAULT 0,
|
|
|
|
-- Costos
|
|
instructor_fee DECIMAL(12, 2) DEFAULT 0,
|
|
materials_cost DECIMAL(12, 2) DEFAULT 0,
|
|
venue_cost DECIMAL(12, 2) DEFAULT 0,
|
|
total_cost DECIMAL(12, 2) GENERATED ALWAYS AS (
|
|
COALESCE(instructor_fee, 0) +
|
|
COALESCE(materials_cost, 0) +
|
|
COALESCE(venue_cost, 0)
|
|
) STORED,
|
|
|
|
-- Feedback
|
|
feedback_summary TEXT,
|
|
average_satisfaction DECIMAL(3, 2), -- Escala 1-5
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}',
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_by UUID REFERENCES core_users.users(id),
|
|
updated_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Soft delete
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Constraints
|
|
CONSTRAINT uq_training_code_tenant UNIQUE (tenant_id, training_code),
|
|
CONSTRAINT chk_training_times CHECK (scheduled_end_time > scheduled_start_time),
|
|
CONSTRAINT chk_training_capacity CHECK (
|
|
max_participants IS NULL OR
|
|
min_participants IS NULL OR
|
|
max_participants >= min_participants
|
|
),
|
|
CONSTRAINT chk_training_score CHECK (
|
|
passing_score IS NULL OR
|
|
(passing_score >= 0 AND passing_score <= 100)
|
|
),
|
|
CONSTRAINT chk_training_participants CHECK (
|
|
registered_participants >= 0 AND
|
|
actual_attendees >= 0 AND
|
|
actual_attendees <= registered_participants
|
|
)
|
|
);
|
|
|
|
-- Comentarios
|
|
COMMENT ON TABLE safety_management.trainings IS 'Sesiones de capacitación de seguridad programadas';
|
|
COMMENT ON COLUMN safety_management.trainings.is_mandatory IS 'Indica si es capacitación obligatoria según normativa';
|
|
COMMENT ON COLUMN safety_management.trainings.certificate_validity_months IS 'Meses de validez del certificado antes de requerir renovación';
|
|
|
|
-- ============================================================================
|
|
-- ÍNDICES
|
|
-- ============================================================================
|
|
|
|
-- Índice obligatorio para RLS
|
|
CREATE INDEX idx_trainings_tenant_id ON safety_management.trainings(tenant_id);
|
|
|
|
-- Índices de relaciones
|
|
CREATE INDEX idx_trainings_project ON safety_management.trainings(project_id);
|
|
CREATE INDEX idx_trainings_instructor ON safety_management.trainings(instructor_id);
|
|
|
|
-- Índices de búsqueda
|
|
CREATE INDEX idx_trainings_code ON safety_management.trainings(training_code);
|
|
CREATE INDEX idx_trainings_type ON safety_management.trainings(training_type);
|
|
CREATE INDEX idx_trainings_status ON safety_management.trainings(status);
|
|
CREATE INDEX idx_trainings_date ON safety_management.trainings(scheduled_date DESC);
|
|
|
|
-- Índice para capacitaciones próximas
|
|
CREATE INDEX idx_trainings_upcoming ON safety_management.trainings(scheduled_date)
|
|
WHERE status = 'scheduled' AND is_active = true;
|
|
|
|
-- Índice para capacitaciones obligatorias
|
|
CREATE INDEX idx_trainings_mandatory ON safety_management.trainings(tenant_id, is_mandatory)
|
|
WHERE is_mandatory = true AND is_active = true;
|
|
|
|
-- Índice para búsqueda de texto en nombre
|
|
CREATE INDEX idx_trainings_name_trgm ON safety_management.trainings
|
|
USING gin (training_name gin_trgm_ops);
|
|
|
|
-- ============================================================================
|
|
-- RLS POLICIES
|
|
-- ============================================================================
|
|
|
|
ALTER TABLE safety_management.trainings ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_select ON safety_management.trainings
|
|
FOR SELECT
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_insert ON safety_management.trainings
|
|
FOR INSERT
|
|
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_update ON safety_management.trainings
|
|
FOR UPDATE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_delete ON safety_management.trainings
|
|
FOR DELETE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- ============================================================================
|
|
-- TRIGGERS
|
|
-- ============================================================================
|
|
|
|
-- Trigger para updated_at
|
|
CREATE TRIGGER trg_trainings_update_timestamp
|
|
BEFORE UPDATE ON safety_management.trainings
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION core_shared.set_updated_at();
|
|
|
|
-- Trigger para notificar capacitaciones próximas
|
|
CREATE TRIGGER trg_trainings_notify_upcoming
|
|
AFTER INSERT OR UPDATE ON safety_management.trainings
|
|
FOR EACH ROW
|
|
WHEN (NEW.scheduled_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '7 days')
|
|
EXECUTE FUNCTION safety_management.notify_upcoming_training();
|
|
```
|
|
|
|
---
|
|
|
|
### 7. training_attendance (Registro de Asistencia)
|
|
|
|
**Descripción:** Registro de asistencia y evaluaciones de participantes en capacitaciones.
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Schema: safety_management
|
|
-- Tabla: training_attendance
|
|
-- Descripción: Registro de asistencia y evaluaciones de capacitaciones
|
|
-- Módulo: MAI-007
|
|
-- ============================================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS safety_management.training_attendance (
|
|
-- Primary Key
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
|
|
-- Multi-tenant
|
|
tenant_id UUID NOT NULL REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Relaciones
|
|
training_id UUID NOT NULL REFERENCES safety_management.trainings(id) ON DELETE CASCADE,
|
|
worker_id UUID NOT NULL, -- FK a tabla de trabajadores
|
|
|
|
-- Datos del participante
|
|
worker_name VARCHAR(200) NOT NULL,
|
|
worker_position VARCHAR(100),
|
|
worker_company VARCHAR(200), -- Empresa (constructora, subcontratista)
|
|
|
|
-- Registro
|
|
registration_date TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
registered_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Asistencia
|
|
attended BOOLEAN NOT NULL DEFAULT false,
|
|
check_in_time TIMESTAMPTZ,
|
|
check_out_time TIMESTAMPTZ,
|
|
attendance_method VARCHAR(50), -- 'manual', 'qr', 'biometric', 'digital_signature'
|
|
|
|
-- Firma
|
|
signature_url TEXT, -- URL de firma digital en S3
|
|
|
|
-- Evaluación
|
|
evaluation_taken BOOLEAN NOT NULL DEFAULT false,
|
|
evaluation_date TIMESTAMPTZ,
|
|
score DECIMAL(5, 2),
|
|
passed BOOLEAN,
|
|
evaluation_answers JSONB DEFAULT '{}',
|
|
|
|
-- Certificado
|
|
certificate_issued BOOLEAN NOT NULL DEFAULT false,
|
|
certificate_number VARCHAR(100),
|
|
certificate_issue_date DATE,
|
|
certificate_expiry_date DATE,
|
|
certificate_url TEXT, -- URL del certificado en S3
|
|
|
|
-- Feedback del participante
|
|
satisfaction_rating INTEGER, -- Escala 1-5
|
|
feedback_comments TEXT,
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}',
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
created_by UUID REFERENCES core_users.users(id),
|
|
updated_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Soft delete
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
deleted_at TIMESTAMPTZ,
|
|
deleted_by UUID REFERENCES core_users.users(id),
|
|
|
|
-- Constraints
|
|
CONSTRAINT uq_training_attendance_worker UNIQUE (training_id, worker_id),
|
|
CONSTRAINT chk_training_attendance_score CHECK (
|
|
score IS NULL OR (score >= 0 AND score <= 100)
|
|
),
|
|
CONSTRAINT chk_training_attendance_rating CHECK (
|
|
satisfaction_rating IS NULL OR
|
|
(satisfaction_rating >= 1 AND satisfaction_rating <= 5)
|
|
),
|
|
CONSTRAINT chk_training_attendance_times CHECK (
|
|
check_out_time IS NULL OR
|
|
check_in_time IS NULL OR
|
|
check_out_time >= check_in_time
|
|
)
|
|
);
|
|
|
|
-- Comentarios
|
|
COMMENT ON TABLE safety_management.training_attendance IS 'Registro de asistencia y evaluaciones de capacitaciones';
|
|
COMMENT ON COLUMN safety_management.training_attendance.attendance_method IS 'Método de registro: manual, QR, biométrico, firma digital';
|
|
COMMENT ON COLUMN safety_management.training_attendance.certificate_expiry_date IS 'Fecha de vencimiento del certificado';
|
|
|
|
-- ============================================================================
|
|
-- ÍNDICES
|
|
-- ============================================================================
|
|
|
|
-- Índice obligatorio para RLS
|
|
CREATE INDEX idx_training_attendance_tenant_id ON safety_management.training_attendance(tenant_id);
|
|
|
|
-- Índices de relaciones
|
|
CREATE INDEX idx_training_attendance_training ON safety_management.training_attendance(training_id);
|
|
CREATE INDEX idx_training_attendance_worker ON safety_management.training_attendance(worker_id);
|
|
|
|
-- Índices de búsqueda
|
|
CREATE INDEX idx_training_attendance_attended ON safety_management.training_attendance(attended);
|
|
CREATE INDEX idx_training_attendance_passed ON safety_management.training_attendance(passed)
|
|
WHERE evaluation_taken = true;
|
|
|
|
-- Índice para certificados próximos a vencer
|
|
CREATE INDEX idx_training_attendance_expiry ON safety_management.training_attendance(certificate_expiry_date)
|
|
WHERE certificate_issued = true AND is_active = true;
|
|
|
|
-- Índice para certificados vigentes por trabajador
|
|
CREATE INDEX idx_training_attendance_valid_certs ON safety_management.training_attendance(worker_id, certificate_expiry_date)
|
|
WHERE certificate_issued = true AND certificate_expiry_date >= CURRENT_DATE;
|
|
|
|
-- Índice GIN para búsqueda en respuestas de evaluación
|
|
CREATE INDEX idx_training_attendance_answers ON safety_management.training_attendance
|
|
USING gin (evaluation_answers jsonb_path_ops);
|
|
|
|
-- ============================================================================
|
|
-- RLS POLICIES
|
|
-- ============================================================================
|
|
|
|
ALTER TABLE safety_management.training_attendance ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY tenant_isolation_select ON safety_management.training_attendance
|
|
FOR SELECT
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_insert ON safety_management.training_attendance
|
|
FOR INSERT
|
|
WITH CHECK (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_update ON safety_management.training_attendance
|
|
FOR UPDATE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
CREATE POLICY tenant_isolation_delete ON safety_management.training_attendance
|
|
FOR DELETE
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
|
|
|
-- ============================================================================
|
|
-- TRIGGERS
|
|
-- ============================================================================
|
|
|
|
-- Trigger para updated_at
|
|
CREATE TRIGGER trg_training_attendance_update_timestamp
|
|
BEFORE UPDATE ON safety_management.training_attendance
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION core_shared.set_updated_at();
|
|
|
|
-- Trigger para calcular passed automáticamente al registrar score
|
|
CREATE TRIGGER trg_training_attendance_calculate_passed
|
|
BEFORE INSERT OR UPDATE ON safety_management.training_attendance
|
|
FOR EACH ROW
|
|
WHEN (NEW.score IS NOT NULL)
|
|
EXECUTE FUNCTION safety_management.calculate_training_passed();
|
|
|
|
-- Trigger para actualizar contadores de la capacitación
|
|
CREATE TRIGGER trg_training_attendance_update_training_stats
|
|
AFTER INSERT OR UPDATE ON safety_management.training_attendance
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION safety_management.update_training_statistics();
|
|
|
|
-- Trigger para alertar vencimiento de certificados
|
|
CREATE TRIGGER trg_training_attendance_alert_expiry
|
|
AFTER INSERT OR UPDATE ON safety_management.training_attendance
|
|
FOR EACH ROW
|
|
WHEN (NEW.certificate_expiry_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '30 days')
|
|
EXECUTE FUNCTION safety_management.alert_certificate_expiry();
|
|
```
|
|
|
|
---
|
|
|
|
## Funciones Auxiliares
|
|
|
|
### 1. calculate_epp_expiry_date()
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Función: calculate_epp_expiry_date
|
|
-- Descripción: Calcula automáticamente la fecha de vencimiento del EPP
|
|
-- ============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION safety_management.calculate_epp_expiry_date()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
v_useful_life_days INTEGER;
|
|
BEGIN
|
|
-- Obtener vida útil del catálogo
|
|
SELECT useful_life_days INTO v_useful_life_days
|
|
FROM safety_management.epp_catalog
|
|
WHERE id = NEW.epp_catalog_id;
|
|
|
|
-- Calcular fecha de vencimiento
|
|
IF v_useful_life_days IS NOT NULL THEN
|
|
NEW.expiry_date := NEW.assignment_date + v_useful_life_days;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION safety_management.calculate_epp_expiry_date() IS
|
|
'Calcula automáticamente la fecha de vencimiento del EPP basado en la vida útil del catálogo';
|
|
```
|
|
|
|
### 2. calculate_inspection_score()
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Función: calculate_inspection_score
|
|
-- Descripción: Calcula el score de cumplimiento de la inspección
|
|
-- ============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION safety_management.calculate_inspection_score()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- Calcular porcentaje de cumplimiento
|
|
IF NEW.total_items_checked > 0 THEN
|
|
NEW.overall_score := ROUND(
|
|
(NEW.conforming_items::DECIMAL / NEW.total_items_checked::DECIMAL) * 100,
|
|
2
|
|
);
|
|
ELSE
|
|
NEW.overall_score := 0;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION safety_management.calculate_inspection_score() IS
|
|
'Calcula el porcentaje de cumplimiento de la inspección automáticamente';
|
|
```
|
|
|
|
### 3. notify_serious_incident()
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Función: notify_serious_incident
|
|
-- Descripción: Envía notificaciones automáticas para incidentes graves
|
|
-- ============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION safety_management.notify_serious_incident()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- Insertar notificación en tabla de notificaciones del core
|
|
-- Notificar a: Coordinador de Seguridad, Residente, Director General
|
|
|
|
-- TODO: Implementar lógica de notificación usando sistema MGN-008
|
|
-- Por ahora, solo registramos en log
|
|
RAISE NOTICE 'INCIDENTE GRAVE REPORTADO: % - Severidad: %', NEW.incident_number, NEW.severity;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION safety_management.notify_serious_incident() IS
|
|
'Envía notificaciones automáticas cuando se reporta un incidente grave o fatal';
|
|
```
|
|
|
|
### 4. update_incident_status_investigated()
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Función: update_incident_status_investigated
|
|
-- Descripción: Actualiza estado del incidente al completar investigación
|
|
-- ============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION safety_management.update_incident_status_investigated()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- Actualizar estado del incidente
|
|
UPDATE safety_management.incidents
|
|
SET
|
|
status = 'investigation_completed',
|
|
updated_at = now()
|
|
WHERE id = NEW.incident_id;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION safety_management.update_incident_status_investigated() IS
|
|
'Actualiza automáticamente el estado del incidente cuando se completa la investigación';
|
|
```
|
|
|
|
### 5. calculate_training_passed()
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Función: calculate_training_passed
|
|
-- Descripción: Determina si el participante aprobó la evaluación
|
|
-- ============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION safety_management.calculate_training_passed()
|
|
RETURNS TRIGGER AS $$
|
|
DECLARE
|
|
v_passing_score DECIMAL(5,2);
|
|
BEGIN
|
|
-- Obtener calificación mínima aprobatoria
|
|
SELECT passing_score INTO v_passing_score
|
|
FROM safety_management.trainings
|
|
WHERE id = NEW.training_id;
|
|
|
|
-- Determinar si aprobó
|
|
IF v_passing_score IS NOT NULL AND NEW.score IS NOT NULL THEN
|
|
NEW.passed := NEW.score >= v_passing_score;
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION safety_management.calculate_training_passed() IS
|
|
'Determina automáticamente si el participante aprobó basado en el passing_score';
|
|
```
|
|
|
|
### 6. update_training_statistics()
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Función: update_training_statistics
|
|
-- Descripción: Actualiza contadores de asistencia y resultados
|
|
-- ============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION safety_management.update_training_statistics()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- Actualizar estadísticas de la capacitación
|
|
UPDATE safety_management.trainings
|
|
SET
|
|
registered_participants = (
|
|
SELECT COUNT(*)
|
|
FROM safety_management.training_attendance
|
|
WHERE training_id = NEW.training_id AND is_active = true
|
|
),
|
|
actual_attendees = (
|
|
SELECT COUNT(*)
|
|
FROM safety_management.training_attendance
|
|
WHERE training_id = NEW.training_id AND attended = true AND is_active = true
|
|
),
|
|
passing_participants = (
|
|
SELECT COUNT(*)
|
|
FROM safety_management.training_attendance
|
|
WHERE training_id = NEW.training_id AND passed = true AND is_active = true
|
|
),
|
|
failing_participants = (
|
|
SELECT COUNT(*)
|
|
FROM safety_management.training_attendance
|
|
WHERE training_id = NEW.training_id AND passed = false AND is_active = true
|
|
),
|
|
average_score = (
|
|
SELECT AVG(score)
|
|
FROM safety_management.training_attendance
|
|
WHERE training_id = NEW.training_id AND score IS NOT NULL AND is_active = true
|
|
),
|
|
updated_at = now()
|
|
WHERE id = NEW.training_id;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION safety_management.update_training_statistics() IS
|
|
'Actualiza automáticamente los contadores y promedios de la capacitación';
|
|
```
|
|
|
|
### 7. alert_certificate_expiry()
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Función: alert_certificate_expiry
|
|
-- Descripción: Genera alertas de certificados próximos a vencer
|
|
-- ============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION safety_management.alert_certificate_expiry()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- TODO: Integrar con sistema de notificaciones MGN-008
|
|
-- Enviar notificación al trabajador y coordinador
|
|
|
|
RAISE NOTICE 'ALERTA: Certificado próximo a vencer - Trabajador: %, Expira: %',
|
|
NEW.worker_id, NEW.certificate_expiry_date;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION safety_management.alert_certificate_expiry() IS
|
|
'Genera alertas cuando un certificado está próximo a vencer (30 días)';
|
|
```
|
|
|
|
### 8. notify_upcoming_training()
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Función: notify_upcoming_training
|
|
-- Descripción: Notifica capacitaciones próximas (7 días)
|
|
-- ============================================================================
|
|
|
|
CREATE OR REPLACE FUNCTION safety_management.notify_upcoming_training()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
-- TODO: Integrar con sistema de notificaciones MGN-008
|
|
-- Notificar a participantes registrados e instructor
|
|
|
|
RAISE NOTICE 'RECORDATORIO: Capacitación próxima - %, Fecha: %',
|
|
NEW.training_name, NEW.scheduled_date;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
COMMENT ON FUNCTION safety_management.notify_upcoming_training() IS
|
|
'Notifica automáticamente capacitaciones programadas en los próximos 7 días';
|
|
```
|
|
|
|
---
|
|
|
|
## Vistas Útiles
|
|
|
|
### 1. v_epp_expiring_soon
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Vista: v_epp_expiring_soon
|
|
-- Descripción: EPP próximo a vencer en los próximos 30 días
|
|
-- ============================================================================
|
|
|
|
CREATE OR REPLACE VIEW safety_management.v_epp_expiring_soon AS
|
|
SELECT
|
|
ea.id,
|
|
ea.tenant_id,
|
|
ea.assignment_number,
|
|
ea.worker_id,
|
|
ec.name AS epp_name,
|
|
ec.epp_type,
|
|
ea.assignment_date,
|
|
ea.expiry_date,
|
|
(ea.expiry_date - CURRENT_DATE) AS days_until_expiry,
|
|
ea.status
|
|
FROM safety_management.epp_assignments ea
|
|
JOIN safety_management.epp_catalog ec ON ea.epp_catalog_id = ec.id
|
|
WHERE ea.status IN ('assigned', 'in_use')
|
|
AND ea.expiry_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '30 days'
|
|
AND ea.is_active = true
|
|
ORDER BY ea.expiry_date ASC;
|
|
|
|
COMMENT ON VIEW safety_management.v_epp_expiring_soon IS
|
|
'EPP asignado que vence en los próximos 30 días';
|
|
```
|
|
|
|
### 2. v_incident_kpi
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Vista: v_incident_kpi
|
|
-- Descripción: KPIs de incidentes por proyecto y período
|
|
-- ============================================================================
|
|
|
|
CREATE OR REPLACE VIEW safety_management.v_incident_kpi AS
|
|
SELECT
|
|
tenant_id,
|
|
project_id,
|
|
DATE_TRUNC('month', incident_date) AS month,
|
|
COUNT(*) AS total_incidents,
|
|
COUNT(*) FILTER (WHERE severity = 'near_miss') AS near_miss_count,
|
|
COUNT(*) FILTER (WHERE severity IN ('minor', 'first_aid')) AS minor_count,
|
|
COUNT(*) FILTER (WHERE severity IN ('lost_time', 'serious')) AS serious_count,
|
|
COUNT(*) FILTER (WHERE severity = 'fatal') AS fatal_count,
|
|
SUM(work_days_lost) AS total_days_lost,
|
|
SUM(total_costs) AS total_incident_costs,
|
|
AVG(work_days_lost) FILTER (WHERE work_days_lost > 0) AS avg_days_lost
|
|
FROM safety_management.incidents
|
|
WHERE is_active = true
|
|
GROUP BY tenant_id, project_id, DATE_TRUNC('month', incident_date);
|
|
|
|
COMMENT ON VIEW safety_management.v_incident_kpi IS
|
|
'KPIs de incidentes agrupados por proyecto y mes';
|
|
```
|
|
|
|
### 3. v_training_compliance
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Vista: v_training_compliance
|
|
-- Descripción: Cumplimiento de capacitaciones por trabajador
|
|
-- ============================================================================
|
|
|
|
CREATE OR REPLACE VIEW safety_management.v_training_compliance AS
|
|
SELECT
|
|
ta.tenant_id,
|
|
ta.worker_id,
|
|
ta.worker_name,
|
|
COUNT(*) AS total_trainings_attended,
|
|
COUNT(*) FILTER (WHERE ta.certificate_issued = true) AS certificates_obtained,
|
|
COUNT(*) FILTER (
|
|
WHERE ta.certificate_expiry_date >= CURRENT_DATE
|
|
) AS valid_certificates,
|
|
COUNT(*) FILTER (
|
|
WHERE ta.certificate_expiry_date < CURRENT_DATE
|
|
) AS expired_certificates,
|
|
MAX(t.scheduled_date) AS last_training_date
|
|
FROM safety_management.training_attendance ta
|
|
JOIN safety_management.trainings t ON ta.training_id = t.id
|
|
WHERE ta.attended = true AND ta.is_active = true
|
|
GROUP BY ta.tenant_id, ta.worker_id, ta.worker_name;
|
|
|
|
COMMENT ON VIEW safety_management.v_training_compliance IS
|
|
'Resumen de cumplimiento de capacitaciones por trabajador';
|
|
```
|
|
|
|
---
|
|
|
|
## Migraciones
|
|
|
|
### Orden de Creación
|
|
|
|
```sql
|
|
-- 1. Crear schema
|
|
CREATE SCHEMA IF NOT EXISTS safety_management;
|
|
|
|
-- 2. Crear ENUMs
|
|
-- (Ver sección de ENUMs arriba)
|
|
|
|
-- 3. Crear tablas en orden de dependencias
|
|
-- 3.1 epp_catalog (sin dependencias externas al schema)
|
|
-- 3.2 epp_assignments (depende de epp_catalog)
|
|
-- 3.3 safety_inspections (sin dependencias internas)
|
|
-- 3.4 incidents (sin dependencias internas)
|
|
-- 3.5 incident_investigations (depende de incidents)
|
|
-- 3.6 trainings (sin dependencias internas)
|
|
-- 3.7 training_attendance (depende de trainings)
|
|
|
|
-- 4. Crear índices
|
|
|
|
-- 5. Habilitar RLS y crear policies
|
|
|
|
-- 6. Crear funciones auxiliares
|
|
|
|
-- 7. Crear triggers
|
|
|
|
-- 8. Crear vistas
|
|
```
|
|
|
|
### Ejemplo de Migración Prisma
|
|
|
|
```typescript
|
|
// prisma/migrations/20251206_create_safety_management_schema.sql
|
|
|
|
-- Step 1: Create schema
|
|
CREATE SCHEMA IF NOT EXISTS safety_management;
|
|
|
|
-- Step 2: Create ENUMs
|
|
CREATE TYPE safety_management.epp_type AS ENUM (
|
|
'helmet', 'safety_glasses', 'goggles', 'face_shield',
|
|
'ear_plugs', 'ear_muffs', 'respirator', 'dust_mask',
|
|
'work_gloves', 'cut_resistant_gloves', 'electrical_gloves',
|
|
'safety_boots', 'rubber_boots', 'safety_vest', 'reflective_vest',
|
|
'harness', 'lifeline', 'rain_suit', 'coveralls', 'welding_jacket', 'other'
|
|
);
|
|
|
|
-- ... (resto de ENUMs)
|
|
|
|
-- Step 3: Create tables
|
|
-- ... (DDL de todas las tablas)
|
|
|
|
-- Step 4: Enable RLS
|
|
-- ... (Políticas RLS)
|
|
|
|
-- Step 5: Create functions and triggers
|
|
-- ... (Funciones y triggers)
|
|
```
|
|
|
|
---
|
|
|
|
## Seeds de Datos Iniciales
|
|
|
|
### 1. Catálogo de EPP Básico
|
|
|
|
```sql
|
|
-- ============================================================================
|
|
-- Seed: Catálogo de EPP básico
|
|
-- ============================================================================
|
|
|
|
INSERT INTO safety_management.epp_catalog
|
|
(tenant_id, code, name, epp_type, useful_life_days, replacement_alert_days, minimum_stock)
|
|
VALUES
|
|
-- Protección de cabeza
|
|
('{tenant_uuid}', 'EPP-001', 'Casco de Seguridad Clase G', 'helmet', 730, 30, 50),
|
|
('{tenant_uuid}', 'EPP-002', 'Casco de Seguridad Clase E', 'helmet', 730, 30, 20),
|
|
|
|
-- Protección visual
|
|
('{tenant_uuid}', 'EPP-003', 'Lentes de Seguridad Clara', 'safety_glasses', 180, 15, 100),
|
|
('{tenant_uuid}', 'EPP-004', 'Lentes de Seguridad Oscura', 'safety_glasses', 180, 15, 50),
|
|
('{tenant_uuid}', 'EPP-005', 'Goggles Antiempañante', 'goggles', 180, 15, 30),
|
|
|
|
-- Protección auditiva
|
|
('{tenant_uuid}', 'EPP-006', 'Tapones Auditivos Desechables', 'ear_plugs', 30, 3, 500),
|
|
('{tenant_uuid}', 'EPP-007', 'Orejeras de Cancelación de Ruido', 'ear_muffs', 365, 30, 30),
|
|
|
|
-- Protección respiratoria
|
|
('{tenant_uuid}', 'EPP-008', 'Cubrebocas N95', 'dust_mask', 7, 1, 500),
|
|
('{tenant_uuid}', 'EPP-009', 'Respirador de Media Cara', 'respirator', 365, 30, 20),
|
|
|
|
-- Protección de manos
|
|
('{tenant_uuid}', 'EPP-010', 'Guantes de Carnaza', 'work_gloves', 60, 10, 200),
|
|
('{tenant_uuid}', 'EPP-011', 'Guantes Anticorte Nivel 5', 'cut_resistant_gloves', 90, 10, 50),
|
|
('{tenant_uuid}', 'EPP-012', 'Guantes Dieléctricos Clase 00', 'electrical_gloves', 180, 30, 20),
|
|
|
|
-- Protección de pies
|
|
('{tenant_uuid}', 'EPP-013', 'Botas de Seguridad Dieléctrica', 'safety_boots', 365, 30, 100),
|
|
('{tenant_uuid}', 'EPP-014', 'Botas de Hule', 'rubber_boots', 365, 30, 50),
|
|
|
|
-- Protección corporal
|
|
('{tenant_uuid}', 'EPP-015', 'Chaleco Reflejante Clase 2', 'reflective_vest', 180, 15, 200),
|
|
('{tenant_uuid}', 'EPP-016', 'Arnés de Cuerpo Completo', 'harness', 730, 60, 50),
|
|
('{tenant_uuid}', 'EPP-017', 'Línea de Vida Retráctil 6m', 'lifeline', 365, 30, 30);
|
|
```
|
|
|
|
---
|
|
|
|
## Validaciones y Constraints
|
|
|
|
### Reglas de Negocio Implementadas
|
|
|
|
1. **EPP:**
|
|
- Vida útil debe ser > 0 días
|
|
- Alerta de reemplazo debe ser antes del vencimiento
|
|
- Código único por tenant
|
|
- Cantidad asignada debe ser > 0
|
|
|
|
2. **Inspecciones:**
|
|
- Total de items = conformes + no conformes + no aplicables
|
|
- No conformes = suma de hallazgos por nivel de riesgo
|
|
- Score entre 0 y 100
|
|
- Número único por tenant
|
|
|
|
3. **Incidentes:**
|
|
- Días perdidos >= 0
|
|
- Costos >= 0
|
|
- Número único por tenant
|
|
- Solo una investigación por incidente
|
|
|
|
4. **Capacitaciones:**
|
|
- Hora fin > hora inicio
|
|
- Capacidad máxima >= mínima
|
|
- Score aprobatorio entre 0 y 100
|
|
- Asistentes <= registrados
|
|
|
|
5. **Asistencia:**
|
|
- Score entre 0 y 100
|
|
- Satisfacción entre 1 y 5
|
|
- Hora salida >= hora entrada
|
|
- Un registro por trabajador por capacitación
|
|
|
|
---
|
|
|
|
## Consideraciones de Rendimiento
|
|
|
|
### Particionamiento
|
|
|
|
Para tablas con alto volumen:
|
|
|
|
```sql
|
|
-- Particionar incidents por fecha (recomendado después de 1 año de uso)
|
|
CREATE TABLE safety_management.incidents (
|
|
-- ... columnas
|
|
) PARTITION BY RANGE (incident_date);
|
|
|
|
-- Crear particiones trimestrales
|
|
CREATE TABLE safety_management.incidents_2025_q1
|
|
PARTITION OF safety_management.incidents
|
|
FOR VALUES FROM ('2025-01-01') TO ('2025-04-01');
|
|
|
|
CREATE TABLE safety_management.incidents_2025_q2
|
|
PARTITION OF safety_management.incidents
|
|
FOR VALUES FROM ('2025-04-01') TO ('2025-07-01');
|
|
```
|
|
|
|
### Índices Optimizados
|
|
|
|
- **Índices compuestos** para consultas frecuentes de KPIs
|
|
- **Índices parciales** para filtrar solo registros activos
|
|
- **Índices GIN** para búsquedas en JSONB y arrays
|
|
- **Índices trigram** para búsqueda de texto
|
|
|
|
### Mantenimiento
|
|
|
|
```sql
|
|
-- Reindexación periódica (mensual)
|
|
REINDEX SCHEMA safety_management;
|
|
|
|
-- Análisis de estadísticas (semanal)
|
|
ANALYZE safety_management.epp_assignments;
|
|
ANALYZE safety_management.safety_inspections;
|
|
ANALYZE safety_management.incidents;
|
|
ANALYZE safety_management.trainings;
|
|
```
|
|
|
|
---
|
|
|
|
## Integración con Otros Módulos
|
|
|
|
### Referencias a Tablas Externas
|
|
|
|
```sql
|
|
-- MAI-001: Fundamentos
|
|
worker_id UUID -- Referencias a workers.id
|
|
project_id UUID -- Referencias a projects.id
|
|
|
|
-- Core Users (MGN-001)
|
|
created_by UUID -- Referencias a core_users.users.id
|
|
inspector_id UUID -- Referencias a core_users.users.id
|
|
reported_by_id UUID -- Referencias a core_users.users.id
|
|
|
|
-- Core Tenants (MGN-003)
|
|
tenant_id UUID NOT NULL -- Referencias a core_tenants.tenants.id
|
|
```
|
|
|
|
### Sincronización con Apps Móviles
|
|
|
|
- Tablas optimizadas para sincronización offline
|
|
- Campos `gps_latitude`, `gps_longitude` para geolocalización
|
|
- Arrays de URLs para fotos capturadas en campo
|
|
- JSONB para datos flexibles de checklists
|
|
|
|
---
|
|
|
|
## Checklist de Validación
|
|
|
|
- [x] Schema `safety_management` creado
|
|
- [x] 8 ENUMs definidos con valores apropiados
|
|
- [x] 7 tablas principales con estructura completa
|
|
- [x] Todas las tablas tienen `tenant_id`
|
|
- [x] RLS habilitado en todas las tablas
|
|
- [x] 4 políticas RLS por tabla (SELECT, INSERT, UPDATE, DELETE)
|
|
- [x] Índices obligatorios en `tenant_id`
|
|
- [x] Índices de performance en columnas de búsqueda
|
|
- [x] Triggers para `updated_at`
|
|
- [x] Triggers de negocio (alertas, cálculos)
|
|
- [x] Funciones auxiliares documentadas
|
|
- [x] Vistas para KPIs y reportes
|
|
- [x] Constraints de integridad y reglas de negocio
|
|
- [x] Comentarios en tablas y columnas críticas
|
|
- [x] Seeds de datos iniciales
|
|
- [x] Consideraciones de rendimiento
|
|
|
|
---
|
|
|
|
## Próximos Pasos
|
|
|
|
1. **Implementación:**
|
|
- Crear migración Prisma
|
|
- Ejecutar en ambiente de desarrollo
|
|
- Validar con datos de prueba
|
|
|
|
2. **Integración:**
|
|
- Definir DTOs de TypeScript
|
|
- Crear Prisma schema
|
|
- Implementar repositorios
|
|
|
|
3. **Testing:**
|
|
- Tests unitarios de funciones
|
|
- Tests de integridad referencial
|
|
- Tests de performance con volumen
|
|
|
|
4. **Documentación:**
|
|
- Diagramas ER
|
|
- Diccionario de datos
|
|
- Guía de consultas frecuentes
|
|
|
|
---
|
|
|
|
**Generado:** 2025-12-06
|
|
**Versión:** 1.0.0
|
|
**Estado:** Completo
|
|
**Mantenedor:** @database-team
|