1327 lines
46 KiB
Markdown
1327 lines
46 KiB
Markdown
# 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 |
|