# 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

```mermaid
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

```sql
-- 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 |

```sql
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 |

```sql
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 |

```sql
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 |

```sql
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 |

```sql
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 |

```sql
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 |

```sql
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 |

```sql
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 |

```sql
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 |

```sql
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

```sql
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

```sql
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

```sql
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)

```sql
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

```sql
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

```sql
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

```sql
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:

```sql
-- 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)

```sql
-- 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

```sql
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

```sql
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

```sql
-- 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 |
