erp-construccion/docs/02-definicion-modulos/MAI-005-control-obra/especificaciones/ET-OBRA-001-database.md

46 KiB

DDL-SPEC: Schema construction_management

Identificacion

Campo Valor
Schema construction_management
Modulo MAI-005
Version 1.0
Estado En Diseno
Autor Requirements-Analyst
Fecha 2025-12-06

Descripcion General

El schema construction_management implementa el sistema de control de obra con programación CPM, Curva S, Earned Value Management (EVM), captura de avances físicos geolocalizados, evidencias fotográficas con hash SHA256, checklists de calidad con firma digital y bitácora de obra.

RF Cubiertos

RF Titulo Tablas
RF-OBRA-001 Programación y Curva S schedules, schedule_activities, s_curve_snapshots
RF-OBRA-002 Captura de Avances work_progress, progress_photos, resource_assignments
RF-OBRA-003 Evidencias y Checklists progress_photos, quality_checklists, non_conformities
RF-OBRA-004 Dashboard y Reportes estimations, kpi_metrics, alerts

Diagrama ER

erDiagram
    schedules {
        uuid id PK
        uuid project_id FK
        uuid tenant_id FK
        varchar name
        date start_date
        date end_date
        varchar status
        boolean is_baseline
        int version
    }

    schedule_activities {
        uuid id PK
        uuid schedule_id FK
        varchar code
        varchar name
        int duration_days
        date early_start
        date early_finish
        date late_start
        date late_finish
        int total_float
        boolean is_critical_path
        decimal percent_complete
        jsonb dependencies
    }

    work_progress {
        uuid id PK
        uuid tenant_id FK
        uuid project_id FK
        uuid activity_id FK
        uuid unit_id FK
        decimal previous_percent
        decimal current_percent
        decimal quantity_delta
        date progress_date
        varchar status
        geometry geolocation
        varchar device_id
        boolean synced
    }

    progress_photos {
        uuid id PK
        uuid tenant_id FK
        uuid progress_id FK
        varchar file_path
        varchar thumbnail_path
        varchar sha256_hash
        geometry geolocation
        jsonb exif_data
        boolean has_watermark
        timestamptz captured_at
    }

    work_log {
        uuid id PK
        uuid tenant_id FK
        uuid project_id FK
        varchar category
        text description
        geometry geolocation
        jsonb multimedia
        timestamptz logged_at
    }

    resource_assignments {
        uuid id PK
        uuid tenant_id FK
        uuid activity_id FK
        uuid resource_id FK
        varchar resource_type
        decimal quantity
        decimal unit_cost
    }

    estimations {
        uuid id PK
        uuid tenant_id FK
        uuid project_id FK
        date snapshot_date
        decimal planned_value
        decimal earned_value
        decimal actual_cost
        decimal spi
        decimal cpi
        decimal eac
        decimal vac
    }

    s_curve_snapshots {
        uuid id PK
        uuid schedule_id FK
        date snapshot_date
        decimal planned_pct
        decimal actual_pct
        decimal spi
        decimal cpi
    }

    quality_checklists {
        uuid id PK
        uuid tenant_id FK
        uuid project_id FK
        uuid unit_id FK
        uuid template_id FK
        jsonb items
        decimal compliance_percent
        text signature_data
        varchar signature_hash
        varchar pdf_path
        timestamptz inspected_at
    }

    non_conformities {
        uuid id PK
        uuid tenant_id FK
        uuid checklist_id FK
        varchar severity
        text description
        text corrective_action
        uuid responsible_id FK
        date deadline
        varchar status
    }

    schedules ||--o{ schedule_activities : "contiene"
    schedules ||--o{ s_curve_snapshots : "snapshots"
    schedule_activities ||--o{ work_progress : "avances"
    work_progress ||--o{ progress_photos : "evidencias"
    schedule_activities ||--o{ resource_assignments : "recursos"
    quality_checklists ||--o{ non_conformities : "NCs"

Extensiones PostgreSQL

PostGIS para Geolocalización

-- Habilitar extensión PostGIS
CREATE EXTENSION IF NOT EXISTS postgis;
CREATE EXTENSION IF NOT EXISTS postgis_topology;

-- Validar instalación
SELECT PostGIS_Version();

Uso en el schema:

  • Geolocalización de avances físicos (validación de radio del sitio)
  • Coordenadas GPS de fotografías con marca de agua
  • Mapas de calor de unidades
  • Validación de capturas dentro del sitio (ST_DWithin)

Tablas

1. schedules

Cronogramas de obra con control de versiones.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
project_id UUID NOT NULL - FK a projects
name VARCHAR(255) NOT NULL - Nombre cronograma
description TEXT NULL - Descripcion
start_date DATE NOT NULL - Fecha inicio
end_date DATE NOT NULL - Fecha fin
status VARCHAR(20) NOT NULL 'draft' Estado
is_baseline BOOLEAN NOT NULL false Es baseline aprobado
version INTEGER NOT NULL 1 Version del cronograma
baseline_date DATE NULL - Fecha aprobacion baseline
approved_by UUID NULL - Usuario aprobador
created_by UUID NOT NULL - Usuario creador
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
updated_at TIMESTAMPTZ NOT NULL NOW() Fecha actualizacion
CREATE TABLE construction_management.schedules (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    project_id UUID NOT NULL,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    start_date DATE NOT NULL,
    end_date DATE NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    is_baseline BOOLEAN NOT NULL DEFAULT false,
    version INTEGER NOT NULL DEFAULT 1,
    baseline_date DATE,
    approved_by UUID,
    created_by UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT chk_schedules_status CHECK (status IN (
        'draft', 'pending_approval', 'active', 'completed', 'cancelled'
    )),
    CONSTRAINT chk_schedules_dates CHECK (end_date > start_date)
);

CREATE INDEX idx_schedules_tenant ON construction_management.schedules(tenant_id);
CREATE INDEX idx_schedules_project ON construction_management.schedules(project_id);
CREATE INDEX idx_schedules_status ON construction_management.schedules(status);
CREATE UNIQUE INDEX idx_schedules_baseline ON construction_management.schedules(project_id)
    WHERE is_baseline = true AND status = 'active';

-- RLS
ALTER TABLE construction_management.schedules ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON construction_management.schedules
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

2. schedule_activities

Actividades del cronograma con datos CPM (Critical Path Method).

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
schedule_id UUID NOT NULL - FK a schedules
code VARCHAR(50) NOT NULL - Codigo WBS
name VARCHAR(255) NOT NULL - Nombre actividad
description TEXT NULL - Descripcion
duration_days INTEGER NOT NULL - Duracion en dias
early_start DATE NULL - ES (CPM)
early_finish DATE NULL - EF (CPM)
late_start DATE NULL - LS (CPM)
late_finish DATE NULL - LF (CPM)
total_float INTEGER NULL - Holgura total
free_float INTEGER NULL - Holgura libre
is_critical_path BOOLEAN NOT NULL false Pertenece a ruta critica
percent_complete DECIMAL(5,2) NOT NULL 0 % Avance
dependencies JSONB NULL '[]' [{predecessor_id, type, lag}]
budget_amount DECIMAL(18,4) NULL 0 Presupuesto asignado
actual_cost DECIMAL(18,4) NOT NULL 0 Costo real acumulado
actual_start DATE NULL - Fecha inicio real
actual_finish DATE NULL - Fecha fin real
responsible_id UUID NULL - Responsable
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
updated_at TIMESTAMPTZ NOT NULL NOW() Fecha actualizacion
CREATE TABLE construction_management.schedule_activities (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    schedule_id UUID NOT NULL REFERENCES construction_management.schedules(id) ON DELETE CASCADE,
    code VARCHAR(50) NOT NULL,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    duration_days INTEGER NOT NULL,
    early_start DATE,
    early_finish DATE,
    late_start DATE,
    late_finish DATE,
    total_float INTEGER,
    free_float INTEGER,
    is_critical_path BOOLEAN NOT NULL DEFAULT false,
    percent_complete DECIMAL(5,2) NOT NULL DEFAULT 0,
    dependencies JSONB DEFAULT '[]',
    budget_amount DECIMAL(18,4) DEFAULT 0,
    actual_cost DECIMAL(18,4) NOT NULL DEFAULT 0,
    actual_start DATE,
    actual_finish DATE,
    responsible_id UUID,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_activities_schedule_code UNIQUE (schedule_id, code),
    CONSTRAINT chk_activities_percent CHECK (percent_complete >= 0 AND percent_complete <= 100),
    CONSTRAINT chk_activities_duration CHECK (duration_days > 0)
);

CREATE INDEX idx_activities_schedule ON construction_management.schedule_activities(schedule_id);
CREATE INDEX idx_activities_critical ON construction_management.schedule_activities(schedule_id, is_critical_path)
    WHERE is_critical_path = true;
CREATE INDEX idx_activities_incomplete ON construction_management.schedule_activities(schedule_id, percent_complete)
    WHERE percent_complete < 100;
CREATE INDEX idx_activities_responsible ON construction_management.schedule_activities(responsible_id);

3. work_progress

Registros de avance físico con geolocalización.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
project_id UUID NOT NULL - FK a projects
activity_id UUID NULL - FK a schedule_activities
unit_id UUID NULL - FK a units (lotes/viviendas)
previous_percent DECIMAL(5,2) NOT NULL 0 % Anterior
current_percent DECIMAL(5,2) NOT NULL 0 % Actual
quantity_delta DECIMAL(18,4) NULL - Cantidad avanzada
unit_measure VARCHAR(20) NULL - Unidad medida
progress_date DATE NOT NULL - Fecha del avance
status VARCHAR(20) NOT NULL 'pending' Estado
geolocation GEOMETRY(Point, 4326) NULL - Coordenadas GPS
distance_from_site INTEGER NULL - Distancia del sitio (m)
notes TEXT NULL - Observaciones
device_id VARCHAR(100) NULL - ID dispositivo movil
synced BOOLEAN NOT NULL true Sincronizado
local_id VARCHAR(100) NULL - ID local (offline)
recorded_by UUID NOT NULL - Usuario registro
approved_by UUID NULL - Usuario aprobador
approved_at TIMESTAMPTZ NULL - Fecha aprobacion
rejection_reason TEXT NULL - Motivo rechazo
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
updated_at TIMESTAMPTZ NOT NULL NOW() Fecha actualizacion
CREATE TABLE construction_management.work_progress (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    project_id UUID NOT NULL,
    activity_id UUID REFERENCES construction_management.schedule_activities(id),
    unit_id UUID,
    previous_percent DECIMAL(5,2) NOT NULL DEFAULT 0,
    current_percent DECIMAL(5,2) NOT NULL DEFAULT 0,
    quantity_delta DECIMAL(18,4),
    unit_measure VARCHAR(20),
    progress_date DATE NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    geolocation GEOMETRY(Point, 4326),
    distance_from_site INTEGER,
    notes TEXT,
    device_id VARCHAR(100),
    synced BOOLEAN NOT NULL DEFAULT true,
    local_id VARCHAR(100),
    recorded_by UUID NOT NULL,
    approved_by UUID,
    approved_at TIMESTAMPTZ,
    rejection_reason TEXT,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT chk_progress_status CHECK (status IN (
        'pending', 'reviewed', 'approved', 'rejected'
    )),
    CONSTRAINT chk_progress_percent CHECK (
        previous_percent >= 0 AND previous_percent <= 100 AND
        current_percent >= 0 AND current_percent <= 100 AND
        current_percent >= previous_percent
    )
);

CREATE INDEX idx_progress_tenant ON construction_management.work_progress(tenant_id);
CREATE INDEX idx_progress_project ON construction_management.work_progress(project_id, progress_date DESC);
CREATE INDEX idx_progress_activity ON construction_management.work_progress(activity_id);
CREATE INDEX idx_progress_status ON construction_management.work_progress(status, created_at DESC)
    WHERE status = 'pending';
CREATE INDEX idx_progress_geolocation ON construction_management.work_progress USING GIST(geolocation)
    WHERE geolocation IS NOT NULL;
CREATE INDEX idx_progress_sync ON construction_management.work_progress(device_id, synced)
    WHERE synced = false;

-- RLS
ALTER TABLE construction_management.work_progress ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON construction_management.work_progress
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

4. progress_photos

Evidencias fotográficas con hash SHA256 y geolocalización.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
progress_id UUID NULL - FK a work_progress
project_id UUID NOT NULL - FK a projects
album_id UUID NULL - FK a albums
file_path VARCHAR(500) NOT NULL - Ruta archivo original
thumbnail_path VARCHAR(500) NULL - Ruta thumbnail
file_size INTEGER NOT NULL - Tamano bytes
sha256_hash VARCHAR(64) NOT NULL - Hash SHA256
mime_type VARCHAR(50) NOT NULL - Tipo MIME
geolocation GEOMETRY(Point, 4326) NULL - Coordenadas GPS
altitude DECIMAL(10,2) NULL - Altitud metros
exif_data JSONB NULL '{}' Metadatos EXIF
has_watermark BOOLEAN NOT NULL false Marca de agua aplicada
watermark_text TEXT NULL - Texto marca de agua
captured_at TIMESTAMPTZ NOT NULL - Fecha captura
uploaded_by UUID NOT NULL - Usuario carga
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE construction_management.progress_photos (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    progress_id UUID REFERENCES construction_management.work_progress(id) ON DELETE SET NULL,
    project_id UUID NOT NULL,
    album_id UUID,
    file_path VARCHAR(500) NOT NULL,
    thumbnail_path VARCHAR(500),
    file_size INTEGER NOT NULL,
    sha256_hash VARCHAR(64) NOT NULL,
    mime_type VARCHAR(50) NOT NULL,
    geolocation GEOMETRY(Point, 4326),
    altitude DECIMAL(10,2),
    exif_data JSONB DEFAULT '{}',
    has_watermark BOOLEAN NOT NULL DEFAULT false,
    watermark_text TEXT,
    captured_at TIMESTAMPTZ NOT NULL,
    uploaded_by UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT chk_photos_hash CHECK (LENGTH(sha256_hash) = 64)
);

CREATE INDEX idx_photos_tenant ON construction_management.progress_photos(tenant_id);
CREATE INDEX idx_photos_progress ON construction_management.progress_photos(progress_id);
CREATE INDEX idx_photos_project ON construction_management.progress_photos(project_id, captured_at DESC);
CREATE INDEX idx_photos_hash ON construction_management.progress_photos(sha256_hash);
CREATE INDEX idx_photos_geolocation ON construction_management.progress_photos USING GIST(geolocation)
    WHERE geolocation IS NOT NULL;
CREATE INDEX idx_photos_album ON construction_management.progress_photos(album_id);

-- RLS
ALTER TABLE construction_management.progress_photos ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON construction_management.progress_photos
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

5. work_log

Bitácora digital de obra.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
project_id UUID NOT NULL - FK a projects
category VARCHAR(50) NOT NULL - Categoria evento
title VARCHAR(255) NOT NULL - Titulo
description TEXT NOT NULL - Descripcion
geolocation GEOMETRY(Point, 4326) NULL - Ubicacion
multimedia JSONB NULL '[]' Fotos/videos/audios
weather_condition VARCHAR(50) NULL - Clima
temperature DECIMAL(5,2) NULL - Temperatura
logged_at TIMESTAMPTZ NOT NULL NOW() Fecha/hora evento
logged_by UUID NOT NULL - Usuario registro
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE construction_management.work_log (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    project_id UUID NOT NULL,
    category VARCHAR(50) NOT NULL,
    title VARCHAR(255) NOT NULL,
    description TEXT NOT NULL,
    geolocation GEOMETRY(Point, 4326),
    multimedia JSONB DEFAULT '[]',
    weather_condition VARCHAR(50),
    temperature DECIMAL(5,2),
    logged_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    logged_by UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT chk_log_category CHECK (category IN (
        'progress', 'incident', 'weather', 'visit', 'meeting', 'delivery', 'other'
    ))
);

CREATE INDEX idx_log_tenant ON construction_management.work_log(tenant_id);
CREATE INDEX idx_log_project ON construction_management.work_log(project_id, logged_at DESC);
CREATE INDEX idx_log_category ON construction_management.work_log(category, logged_at DESC);
CREATE INDEX idx_log_geolocation ON construction_management.work_log USING GIST(geolocation)
    WHERE geolocation IS NOT NULL;

-- RLS
ALTER TABLE construction_management.work_log ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON construction_management.work_log
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

6. resource_assignments

Asignación de recursos a actividades.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
activity_id UUID NOT NULL - FK a schedule_activities
resource_id UUID NOT NULL - FK a recursos
resource_type VARCHAR(20) NOT NULL - Tipo recurso
quantity DECIMAL(18,4) NOT NULL - Cantidad asignada
unit_cost DECIMAL(18,4) NOT NULL - Costo unitario
total_cost DECIMAL(18,4) NOT NULL - Costo total
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE construction_management.resource_assignments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    activity_id UUID NOT NULL REFERENCES construction_management.schedule_activities(id) ON DELETE CASCADE,
    resource_id UUID NOT NULL,
    resource_type VARCHAR(20) NOT NULL,
    quantity DECIMAL(18,4) NOT NULL,
    unit_cost DECIMAL(18,4) NOT NULL,
    total_cost DECIMAL(18,4) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT chk_resource_type CHECK (resource_type IN (
        'labor', 'material', 'equipment', 'subcontractor'
    )),
    CONSTRAINT chk_resource_quantity CHECK (quantity > 0)
);

CREATE INDEX idx_resources_tenant ON construction_management.resource_assignments(tenant_id);
CREATE INDEX idx_resources_activity ON construction_management.resource_assignments(activity_id);
CREATE INDEX idx_resources_type ON construction_management.resource_assignments(resource_type, resource_id);

-- RLS
ALTER TABLE construction_management.resource_assignments ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON construction_management.resource_assignments
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

7. estimations

Métricas de Earned Value Management (EVM).

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
project_id UUID NOT NULL - FK a projects
schedule_id UUID NOT NULL - FK a schedules
snapshot_date DATE NOT NULL - Fecha snapshot
planned_value DECIMAL(18,4) NOT NULL - PV (Valor planeado)
earned_value DECIMAL(18,4) NOT NULL - EV (Valor ganado)
actual_cost DECIMAL(18,4) NOT NULL - AC (Costo real)
budget_at_completion DECIMAL(18,4) NOT NULL - BAC (Presupuesto total)
spi DECIMAL(10,4) NOT NULL - SPI = EV / PV
cpi DECIMAL(10,4) NOT NULL - CPI = EV / AC
schedule_variance DECIMAL(18,4) NOT NULL - SV = EV - PV
cost_variance DECIMAL(18,4) NOT NULL - CV = EV - AC
eac DECIMAL(18,4) NOT NULL - EAC (Estimado al completar)
etc DECIMAL(18,4) NOT NULL - ETC (Estimado para completar)
vac DECIMAL(18,4) NOT NULL - VAC = BAC - EAC
tcpi DECIMAL(10,4) NULL - TCPI (To Complete Perf Index)
percent_complete DECIMAL(5,2) NOT NULL - % Avance físico
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE construction_management.estimations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    project_id UUID NOT NULL,
    schedule_id UUID NOT NULL REFERENCES construction_management.schedules(id),
    snapshot_date DATE NOT NULL,
    planned_value DECIMAL(18,4) NOT NULL,
    earned_value DECIMAL(18,4) NOT NULL,
    actual_cost DECIMAL(18,4) NOT NULL,
    budget_at_completion DECIMAL(18,4) NOT NULL,
    spi DECIMAL(10,4) NOT NULL,
    cpi DECIMAL(10,4) NOT NULL,
    schedule_variance DECIMAL(18,4) NOT NULL,
    cost_variance DECIMAL(18,4) NOT NULL,
    eac DECIMAL(18,4) NOT NULL,
    etc DECIMAL(18,4) NOT NULL,
    vac DECIMAL(18,4) NOT NULL,
    tcpi DECIMAL(10,4),
    percent_complete DECIMAL(5,2) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_estimations_snapshot UNIQUE (schedule_id, snapshot_date)
);

CREATE INDEX idx_estimations_tenant ON construction_management.estimations(tenant_id);
CREATE INDEX idx_estimations_project ON construction_management.estimations(project_id, snapshot_date DESC);
CREATE INDEX idx_estimations_schedule ON construction_management.estimations(schedule_id, snapshot_date DESC);

-- RLS
ALTER TABLE construction_management.estimations ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON construction_management.estimations
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

8. s_curve_snapshots

Snapshots de Curva S para gráficas.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
schedule_id UUID NOT NULL - FK a schedules
snapshot_date DATE NOT NULL - Fecha snapshot
planned_pct DECIMAL(5,2) NOT NULL - % Planeado acumulado
actual_pct DECIMAL(5,2) NOT NULL - % Real acumulado
spi DECIMAL(10,4) NOT NULL - SPI del dia
cpi DECIMAL(10,4) NOT NULL - CPI del dia
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE construction_management.s_curve_snapshots (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    schedule_id UUID NOT NULL REFERENCES construction_management.schedules(id) ON DELETE CASCADE,
    snapshot_date DATE NOT NULL,
    planned_pct DECIMAL(5,2) NOT NULL,
    actual_pct DECIMAL(5,2) NOT NULL,
    spi DECIMAL(10,4) NOT NULL,
    cpi DECIMAL(10,4) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_scurve_snapshot UNIQUE (schedule_id, snapshot_date),
    CONSTRAINT chk_scurve_pct CHECK (
        planned_pct >= 0 AND planned_pct <= 100 AND
        actual_pct >= 0 AND actual_pct <= 100
    )
);

CREATE INDEX idx_scurve_schedule ON construction_management.s_curve_snapshots(schedule_id, snapshot_date);

9. quality_checklists

Checklists de calidad con firma digital.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
project_id UUID NOT NULL - FK a projects
unit_id UUID NULL - FK a units
template_id UUID NOT NULL - FK a checklist_templates
template_name VARCHAR(255) NOT NULL - Nombre template
items JSONB NOT NULL '[]' Items evaluados
total_items INTEGER NOT NULL - Total items
conforming_items INTEGER NOT NULL - Items conformes
compliance_percent DECIMAL(5,2) NOT NULL - % Cumplimiento
signature_data TEXT NULL - Firma Base64
signature_hash VARCHAR(64) NULL - Hash firma
signed_by UUID NULL - Usuario firmante
pdf_path VARCHAR(500) NULL - Ruta PDF generado
inspected_at TIMESTAMPTZ NOT NULL NOW() Fecha inspeccion
created_by UUID NOT NULL - Inspector
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE construction_management.quality_checklists (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    project_id UUID NOT NULL,
    unit_id UUID,
    template_id UUID NOT NULL,
    template_name VARCHAR(255) NOT NULL,
    items JSONB NOT NULL DEFAULT '[]',
    total_items INTEGER NOT NULL,
    conforming_items INTEGER NOT NULL,
    compliance_percent DECIMAL(5,2) NOT NULL,
    signature_data TEXT,
    signature_hash VARCHAR(64),
    signed_by UUID,
    pdf_path VARCHAR(500),
    inspected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT chk_checklist_compliance CHECK (compliance_percent >= 0 AND compliance_percent <= 100)
);

CREATE INDEX idx_checklists_tenant ON construction_management.quality_checklists(tenant_id);
CREATE INDEX idx_checklists_project ON construction_management.quality_checklists(project_id, inspected_at DESC);
CREATE INDEX idx_checklists_unit ON construction_management.quality_checklists(unit_id);
CREATE INDEX idx_checklists_compliance ON construction_management.quality_checklists(compliance_percent)
    WHERE compliance_percent < 95;

-- RLS
ALTER TABLE construction_management.quality_checklists ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON construction_management.quality_checklists
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

10. non_conformities

No Conformidades (NCs) detectadas en inspecciones.

Columna Tipo Nullable Default Descripcion
id UUID NOT NULL gen_random_uuid() PK
tenant_id UUID NOT NULL - FK a tenants
checklist_id UUID NOT NULL - FK a quality_checklists
project_id UUID NOT NULL - FK a projects
nc_number VARCHAR(50) NOT NULL - Numero NC
severity VARCHAR(20) NOT NULL - Severidad
description TEXT NOT NULL - Descripcion
corrective_action TEXT NOT NULL - Accion correctiva
responsible_id UUID NOT NULL - Responsable
deadline DATE NOT NULL - Fecha limite
status VARCHAR(20) NOT NULL 'open' Estado
verification_photos JSONB NULL '[]' Fotos de cierre
closed_at TIMESTAMPTZ NULL - Fecha cierre
closed_by UUID NULL - Usuario cierre
created_at TIMESTAMPTZ NOT NULL NOW() Fecha creacion
CREATE TABLE construction_management.non_conformities (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    checklist_id UUID NOT NULL REFERENCES construction_management.quality_checklists(id),
    project_id UUID NOT NULL,
    nc_number VARCHAR(50) NOT NULL,
    severity VARCHAR(20) NOT NULL,
    description TEXT NOT NULL,
    corrective_action TEXT NOT NULL,
    responsible_id UUID NOT NULL,
    deadline DATE NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'open',
    verification_photos JSONB DEFAULT '[]',
    closed_at TIMESTAMPTZ,
    closed_by UUID,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_nc_number UNIQUE (tenant_id, nc_number),
    CONSTRAINT chk_nc_severity CHECK (severity IN ('minor', 'major', 'critical')),
    CONSTRAINT chk_nc_status CHECK (status IN ('open', 'in_progress', 'closed', 'cancelled'))
);

CREATE INDEX idx_nc_tenant ON construction_management.non_conformities(tenant_id);
CREATE INDEX idx_nc_checklist ON construction_management.non_conformities(checklist_id);
CREATE INDEX idx_nc_project ON construction_management.non_conformities(project_id, created_at DESC);
CREATE INDEX idx_nc_responsible ON construction_management.non_conformities(responsible_id, status);
CREATE INDEX idx_nc_open ON construction_management.non_conformities(tenant_id, status, deadline)
    WHERE status IN ('open', 'in_progress');

-- RLS
ALTER TABLE construction_management.non_conformities ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON construction_management.non_conformities
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

Triggers

1. Actualizar schedule_activities.percent_complete al aprobar avances

CREATE OR REPLACE FUNCTION construction_management.update_activity_progress()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.status = 'approved' AND OLD.status != 'approved' THEN
        UPDATE construction_management.schedule_activities
        SET percent_complete = NEW.current_percent,
            updated_at = NOW()
        WHERE id = NEW.activity_id;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_update_activity_progress
AFTER UPDATE OF status ON construction_management.work_progress
FOR EACH ROW
WHEN (NEW.status = 'approved' AND OLD.status != 'approved')
EXECUTE FUNCTION construction_management.update_activity_progress();

2. Calcular compliance_percent en checklists

CREATE OR REPLACE FUNCTION construction_management.calculate_compliance()
RETURNS TRIGGER AS $$
BEGIN
    NEW.compliance_percent := CASE
        WHEN NEW.total_items > 0 THEN
            (NEW.conforming_items::DECIMAL / NEW.total_items::DECIMAL) * 100
        ELSE 0
    END;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_calculate_compliance
BEFORE INSERT OR UPDATE ON construction_management.quality_checklists
FOR EACH ROW
EXECUTE FUNCTION construction_management.calculate_compliance();

3. Actualizar timestamps

CREATE OR REPLACE FUNCTION construction_management.update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_schedules_timestamp
BEFORE UPDATE ON construction_management.schedules
FOR EACH ROW
EXECUTE FUNCTION construction_management.update_timestamp();

CREATE TRIGGER trg_activities_timestamp
BEFORE UPDATE ON construction_management.schedule_activities
FOR EACH ROW
EXECUTE FUNCTION construction_management.update_timestamp();

CREATE TRIGGER trg_progress_timestamp
BEFORE UPDATE ON construction_management.work_progress
FOR EACH ROW
EXECUTE FUNCTION construction_management.update_timestamp();

Funciones de Utilidad

1. Calcular CPM (Critical Path Method)

CREATE OR REPLACE FUNCTION construction_management.calculate_cpm(
    p_schedule_id UUID
) RETURNS BOOLEAN AS $$
DECLARE
    v_activity RECORD;
    v_max_ef DATE;
    v_min_ls DATE;
BEGIN
    -- Reset valores CPM
    UPDATE construction_management.schedule_activities
    SET early_start = NULL,
        early_finish = NULL,
        late_start = NULL,
        late_finish = NULL,
        total_float = NULL,
        free_float = NULL,
        is_critical_path = false
    WHERE schedule_id = p_schedule_id;

    -- FORWARD PASS: Calcular ES y EF
    FOR v_activity IN
        SELECT id, duration_days, dependencies
        FROM construction_management.schedule_activities
        WHERE schedule_id = p_schedule_id
        ORDER BY id
    LOOP
        -- Calcular ES basado en predecesores
        SELECT COALESCE(MAX(a.early_finish + COALESCE((d->>'lag')::INTEGER, 0)),
                        (SELECT start_date FROM construction_management.schedules WHERE id = p_schedule_id))
        INTO v_activity.early_start
        FROM construction_management.schedule_activities a,
             jsonb_array_elements(v_activity.dependencies) d
        WHERE a.id = (d->>'predecessor_id')::UUID;

        -- Calcular EF = ES + duration
        v_activity.early_finish := v_activity.early_start + v_activity.duration_days;

        -- Actualizar
        UPDATE construction_management.schedule_activities
        SET early_start = v_activity.early_start,
            early_finish = v_activity.early_finish
        WHERE id = v_activity.id;
    END LOOP;

    -- BACKWARD PASS: Calcular LS y LF
    SELECT MAX(early_finish) INTO v_max_ef
    FROM construction_management.schedule_activities
    WHERE schedule_id = p_schedule_id;

    FOR v_activity IN
        SELECT id, duration_days, early_finish
        FROM construction_management.schedule_activities
        WHERE schedule_id = p_schedule_id
        ORDER BY id DESC
    LOOP
        -- Si no tiene sucesores, LF = max EF del proyecto
        SELECT COALESCE(MIN(a.late_start - COALESCE((d->>'lag')::INTEGER, 0)), v_max_ef)
        INTO v_activity.late_finish
        FROM construction_management.schedule_activities a,
             jsonb_array_elements(a.dependencies) d
        WHERE (d->>'predecessor_id')::UUID = v_activity.id;

        -- LS = LF - duration
        v_activity.late_start := v_activity.late_finish - v_activity.duration_days;

        -- Total Float = LF - EF (o LS - ES)
        v_activity.total_float := v_activity.late_finish - v_activity.early_finish;

        -- Critical Path: TF = 0
        v_activity.is_critical_path := (v_activity.total_float = 0);

        -- Actualizar
        UPDATE construction_management.schedule_activities
        SET late_start = v_activity.late_start,
            late_finish = v_activity.late_finish,
            total_float = v_activity.total_float,
            is_critical_path = v_activity.is_critical_path
        WHERE id = v_activity.id;
    END LOOP;

    RETURN TRUE;
END;
$$ LANGUAGE plpgsql;

2. Calcular métricas EVM

CREATE OR REPLACE FUNCTION construction_management.calculate_evm(
    p_schedule_id UUID,
    p_snapshot_date DATE DEFAULT CURRENT_DATE
) RETURNS UUID AS $$
DECLARE
    v_project_id UUID;
    v_tenant_id UUID;
    v_bac DECIMAL(18,4);
    v_pv DECIMAL(18,4);
    v_ev DECIMAL(18,4);
    v_ac DECIMAL(18,4);
    v_spi DECIMAL(10,4);
    v_cpi DECIMAL(10,4);
    v_sv DECIMAL(18,4);
    v_cv DECIMAL(18,4);
    v_eac DECIMAL(18,4);
    v_etc DECIMAL(18,4);
    v_vac DECIMAL(18,4);
    v_tcpi DECIMAL(10,4);
    v_pct DECIMAL(5,2);
    v_estimation_id UUID;
BEGIN
    -- Obtener datos del schedule
    SELECT s.project_id, s.tenant_id INTO v_project_id, v_tenant_id
    FROM construction_management.schedules s
    WHERE s.id = p_schedule_id;

    -- BAC: Budget at Completion (suma de budget de todas las actividades)
    SELECT COALESCE(SUM(budget_amount), 0) INTO v_bac
    FROM construction_management.schedule_activities
    WHERE schedule_id = p_schedule_id;

    -- PV: Planned Value (valor que debería estar completado según cronograma)
    SELECT COALESCE(SUM(
        budget_amount *
        CASE
            WHEN p_snapshot_date >= early_finish THEN 1.0
            WHEN p_snapshot_date <= early_start THEN 0.0
            ELSE (p_snapshot_date - early_start)::DECIMAL / duration_days
        END
    ), 0) INTO v_pv
    FROM construction_management.schedule_activities
    WHERE schedule_id = p_schedule_id;

    -- EV: Earned Value (valor del trabajo realmente completado)
    SELECT COALESCE(SUM(budget_amount * percent_complete / 100.0), 0) INTO v_ev
    FROM construction_management.schedule_activities
    WHERE schedule_id = p_schedule_id;

    -- AC: Actual Cost (costo real incurrido)
    SELECT COALESCE(SUM(actual_cost), 0) INTO v_ac
    FROM construction_management.schedule_activities
    WHERE schedule_id = p_schedule_id;

    -- % Avance físico
    v_pct := CASE WHEN v_bac > 0 THEN (v_ev / v_bac) * 100 ELSE 0 END;

    -- Métricas
    v_spi := CASE WHEN v_pv > 0 THEN v_ev / v_pv ELSE 0 END;
    v_cpi := CASE WHEN v_ac > 0 THEN v_ev / v_ac ELSE 0 END;
    v_sv := v_ev - v_pv;
    v_cv := v_ev - v_ac;

    -- EAC: Estimate at Completion
    v_eac := CASE
        WHEN v_cpi > 0 THEN v_bac / v_cpi
        ELSE v_bac
    END;

    -- ETC: Estimate to Complete
    v_etc := v_eac - v_ac;

    -- VAC: Variance at Completion
    v_vac := v_bac - v_eac;

    -- TCPI: To-Complete Performance Index
    v_tcpi := CASE
        WHEN (v_bac - v_ac) > 0 THEN (v_bac - v_ev) / (v_bac - v_ac)
        ELSE 0
    END;

    -- Insertar snapshot
    INSERT INTO construction_management.estimations (
        tenant_id, project_id, schedule_id, snapshot_date,
        planned_value, earned_value, actual_cost, budget_at_completion,
        spi, cpi, schedule_variance, cost_variance,
        eac, etc, vac, tcpi, percent_complete
    ) VALUES (
        v_tenant_id, v_project_id, p_schedule_id, p_snapshot_date,
        v_pv, v_ev, v_ac, v_bac,
        v_spi, v_cpi, v_sv, v_cv,
        v_eac, v_etc, v_vac, v_tcpi, v_pct
    )
    ON CONFLICT (schedule_id, snapshot_date) DO UPDATE
    SET planned_value = EXCLUDED.planned_value,
        earned_value = EXCLUDED.earned_value,
        actual_cost = EXCLUDED.actual_cost,
        spi = EXCLUDED.spi,
        cpi = EXCLUDED.cpi,
        schedule_variance = EXCLUDED.schedule_variance,
        cost_variance = EXCLUDED.cost_variance,
        eac = EXCLUDED.eac,
        etc = EXCLUDED.etc,
        vac = EXCLUDED.vac,
        tcpi = EXCLUDED.tcpi,
        percent_complete = EXCLUDED.percent_complete
    RETURNING id INTO v_estimation_id;

    RETURN v_estimation_id;
END;
$$ LANGUAGE plpgsql;

3. Generar snapshot de Curva S

CREATE OR REPLACE FUNCTION construction_management.generate_scurve_snapshot(
    p_schedule_id UUID,
    p_snapshot_date DATE DEFAULT CURRENT_DATE
) RETURNS UUID AS $$
DECLARE
    v_planned_pct DECIMAL(5,2);
    v_actual_pct DECIMAL(5,2);
    v_bac DECIMAL(18,4);
    v_pv DECIMAL(18,4);
    v_ev DECIMAL(18,4);
    v_ac DECIMAL(18,4);
    v_spi DECIMAL(10,4);
    v_cpi DECIMAL(10,4);
    v_snapshot_id UUID;
BEGIN
    -- Obtener BAC
    SELECT COALESCE(SUM(budget_amount), 0) INTO v_bac
    FROM construction_management.schedule_activities
    WHERE schedule_id = p_schedule_id;

    -- Calcular PV y EV
    SELECT
        COALESCE(SUM(
            budget_amount *
            CASE
                WHEN p_snapshot_date >= early_finish THEN 1.0
                WHEN p_snapshot_date <= early_start THEN 0.0
                ELSE (p_snapshot_date - early_start)::DECIMAL / NULLIF(duration_days, 0)
            END
        ), 0),
        COALESCE(SUM(budget_amount * percent_complete / 100.0), 0),
        COALESCE(SUM(actual_cost), 0)
    INTO v_pv, v_ev, v_ac
    FROM construction_management.schedule_activities
    WHERE schedule_id = p_schedule_id;

    -- Calcular porcentajes
    v_planned_pct := CASE WHEN v_bac > 0 THEN (v_pv / v_bac) * 100 ELSE 0 END;
    v_actual_pct := CASE WHEN v_bac > 0 THEN (v_ev / v_bac) * 100 ELSE 0 END;

    -- Calcular índices
    v_spi := CASE WHEN v_pv > 0 THEN v_ev / v_pv ELSE 0 END;
    v_cpi := CASE WHEN v_ac > 0 THEN v_ev / v_ac ELSE 0 END;

    -- Insertar snapshot
    INSERT INTO construction_management.s_curve_snapshots (
        schedule_id, snapshot_date, planned_pct, actual_pct, spi, cpi
    ) VALUES (
        p_schedule_id, p_snapshot_date, v_planned_pct, v_actual_pct, v_spi, v_cpi
    )
    ON CONFLICT (schedule_id, snapshot_date) DO UPDATE
    SET planned_pct = EXCLUDED.planned_pct,
        actual_pct = EXCLUDED.actual_pct,
        spi = EXCLUDED.spi,
        cpi = EXCLUDED.cpi
    RETURNING id INTO v_snapshot_id;

    RETURN v_snapshot_id;
END;
$$ LANGUAGE plpgsql;

4. Validar ubicación GPS

CREATE OR REPLACE FUNCTION construction_management.validate_geolocation(
    p_progress_id UUID
) RETURNS BOOLEAN AS $$
DECLARE
    v_project_location GEOMETRY;
    v_progress_location GEOMETRY;
    v_distance INTEGER;
    v_threshold INTEGER := 500; -- 500 metros
BEGIN
    -- Obtener ubicación del proyecto y del avance
    SELECT p.site_location, wp.geolocation
    INTO v_project_location, v_progress_location
    FROM construction_management.work_progress wp
    JOIN projects p ON p.id = wp.project_id
    WHERE wp.id = p_progress_id;

    IF v_project_location IS NULL OR v_progress_location IS NULL THEN
        RETURN NULL;
    END IF;

    -- Calcular distancia en metros
    v_distance := ST_Distance(
        v_project_location::geography,
        v_progress_location::geography
    )::INTEGER;

    -- Actualizar distancia
    UPDATE construction_management.work_progress
    SET distance_from_site = v_distance
    WHERE id = p_progress_id;

    -- Validar umbral
    RETURN v_distance <= v_threshold;
END;
$$ LANGUAGE plpgsql;

RLS Policies

Todas las tablas principales tienen habilitado Row Level Security con políticas de aislamiento por tenant:

-- Ya aplicadas en cada tabla, ejemplo:
ALTER TABLE construction_management.work_progress ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON construction_management.work_progress
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

Jobs Programados

1. Snapshot diario de Curva S (23:00)

-- Job de cron (implementar con pg_cron o scheduler externo)
-- Ejecutar diariamente a las 23:00
CREATE OR REPLACE FUNCTION construction_management.daily_scurve_job()
RETURNS VOID AS $$
DECLARE
    v_schedule RECORD;
BEGIN
    FOR v_schedule IN
        SELECT id FROM construction_management.schedules
        WHERE status = 'active' AND is_baseline = true
    LOOP
        PERFORM construction_management.generate_scurve_snapshot(v_schedule.id, CURRENT_DATE);
    END LOOP;
END;
$$ LANGUAGE plpgsql;

-- Configurar con pg_cron:
-- SELECT cron.schedule('daily-scurve', '0 23 * * *',
--   'SELECT construction_management.daily_scurve_job()');

2. Cálculo diario de EVM

CREATE OR REPLACE FUNCTION construction_management.daily_evm_job()
RETURNS VOID AS $$
DECLARE
    v_schedule RECORD;
BEGIN
    FOR v_schedule IN
        SELECT id FROM construction_management.schedules
        WHERE status = 'active' AND is_baseline = true
    LOOP
        PERFORM construction_management.calculate_evm(v_schedule.id, CURRENT_DATE);
    END LOOP;
END;
$$ LANGUAGE plpgsql;

-- Configurar con pg_cron:
-- SELECT cron.schedule('daily-evm', '0 23 * * *',
--   'SELECT construction_management.daily_evm_job()');

Vistas Materializadas

1. Vista de resumen de proyectos

CREATE MATERIALIZED VIEW construction_management.mv_project_summary AS
SELECT
    s.project_id,
    s.id as schedule_id,
    s.name as schedule_name,
    COUNT(sa.id) as total_activities,
    COUNT(CASE WHEN sa.is_critical_path THEN 1 END) as critical_activities,
    AVG(sa.percent_complete) as avg_progress,
    SUM(sa.budget_amount) as total_budget,
    SUM(sa.actual_cost) as total_actual_cost,
    MAX(e.spi) as latest_spi,
    MAX(e.cpi) as latest_cpi,
    MAX(e.eac) as latest_eac
FROM construction_management.schedules s
JOIN construction_management.schedule_activities sa ON sa.schedule_id = s.id
LEFT JOIN LATERAL (
    SELECT * FROM construction_management.estimations
    WHERE schedule_id = s.id
    ORDER BY snapshot_date DESC LIMIT 1
) e ON true
WHERE s.status = 'active' AND s.is_baseline = true
GROUP BY s.project_id, s.id, s.name;

CREATE UNIQUE INDEX idx_mv_project_summary_pk ON construction_management.mv_project_summary(project_id, schedule_id);

-- Refresh periódico
-- SELECT cron.schedule('refresh-project-summary', '*/30 * * * *',
--   'REFRESH MATERIALIZED VIEW CONCURRENTLY construction_management.mv_project_summary');

Seed Data

Checklist Templates Base

-- Tabla de templates (crear si no existe)
CREATE TABLE IF NOT EXISTS construction_management.checklist_templates (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID,
    name VARCHAR(255) NOT NULL,
    category VARCHAR(100),
    items JSONB NOT NULL DEFAULT '[]',
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

INSERT INTO construction_management.checklist_templates (name, category, items) VALUES
('Acabados de Vivienda', 'quality',
 '[
    {"id": 1, "type": "boolean", "label": "Pintura uniforme", "required": true},
    {"id": 2, "type": "numeric", "label": "Espesor de piso (cm)", "tolerance": {"min": 9, "max": 11}, "required": true},
    {"id": 3, "type": "boolean", "label": "Puertas operan correctamente", "required": true},
    {"id": 4, "type": "boolean", "label": "Ventanas herméticas", "required": true},
    {"id": 5, "type": "photo", "label": "Foto general de sala", "required": true}
  ]'::jsonb),

('Estructura de Concreto', 'structural',
 '[
    {"id": 1, "type": "numeric", "label": "Resistencia concreto (kg/cm²)", "tolerance": {"min": 200, "max": 250}, "required": true},
    {"id": 2, "type": "boolean", "label": "Varillas según plano", "required": true},
    {"id": 3, "type": "numeric", "label": "Recubrimiento (cm)", "tolerance": {"min": 2, "max": 3}, "required": true},
    {"id": 4, "type": "photo", "label": "Foto de armado de acero", "required": true}
  ]'::jsonb);

Historial

Version Fecha Autor Cambios
1.0 2025-12-06 Requirements-Analyst Creacion inicial