- Create field_service schema with 11 tables - Add 5 enums: checklist_item_type, diagnosis_type, sync_status, checklist_status, worklog_status - Add tables: service_checklists, checklist_item_templates, checklist_responses, checklist_item_responses, work_logs, activity_catalog, diagnosis_records, root_cause_catalog, field_evidence, offline_queue_items, field_checkins - Add 4 views: vw_active_work_logs, vw_pending_sync, vw_technician_activity, vw_checklist_summary - Add triggers for duration calculation and count updates - Add RLS policies for multi-tenant isolation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
840 lines
30 KiB
PL/PgSQL
840 lines
30 KiB
PL/PgSQL
-- =============================================================================
|
|
-- 17-field-service-schema.sql
|
|
-- MMD-012: Field Service / Bitácora Técnica Móvil
|
|
-- =============================================================================
|
|
-- Schema: field_service
|
|
-- Descripción: Checklist de servicio, registro de diagnóstico, control de mano
|
|
-- de obra, modo offline con sincronización
|
|
-- Sistema: ERP Mecánicas Diesel
|
|
-- Fecha: 2026-01-27
|
|
-- =============================================================================
|
|
|
|
-- Create schema
|
|
CREATE SCHEMA IF NOT EXISTS field_service;
|
|
|
|
-- =============================================================================
|
|
-- TYPES/ENUMS
|
|
-- =============================================================================
|
|
|
|
-- Tipo de item de checklist
|
|
CREATE TYPE field_service.checklist_item_type AS ENUM (
|
|
'BOOLEAN',
|
|
'TEXT',
|
|
'NUMBER',
|
|
'PHOTO',
|
|
'SIGNATURE',
|
|
'SELECT'
|
|
);
|
|
|
|
-- Tipo de diagnóstico
|
|
CREATE TYPE field_service.diagnosis_type AS ENUM (
|
|
'VISUAL',
|
|
'OBD',
|
|
'ELECTRONIC',
|
|
'MECHANICAL'
|
|
);
|
|
|
|
-- Estado de sincronización
|
|
CREATE TYPE field_service.sync_status AS ENUM (
|
|
'PENDING',
|
|
'IN_PROGRESS',
|
|
'COMPLETED',
|
|
'FAILED',
|
|
'CONFLICT'
|
|
);
|
|
|
|
-- Estado de checklist response
|
|
CREATE TYPE field_service.checklist_status AS ENUM (
|
|
'STARTED',
|
|
'IN_PROGRESS',
|
|
'COMPLETED',
|
|
'CANCELLED'
|
|
);
|
|
|
|
-- Estado de work log
|
|
CREATE TYPE field_service.worklog_status AS ENUM (
|
|
'ACTIVE',
|
|
'PAUSED',
|
|
'COMPLETED',
|
|
'CANCELLED'
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- TABLE: service_checklists (Plantillas de checklist)
|
|
-- =============================================================================
|
|
CREATE TABLE field_service.service_checklists (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
-- Identification
|
|
name VARCHAR(150) NOT NULL,
|
|
description TEXT,
|
|
code VARCHAR(50),
|
|
|
|
-- Scope
|
|
service_type_code VARCHAR(50), -- Aplica a tipo de servicio específico
|
|
vehicle_type VARCHAR(50), -- Aplica a tipo de vehículo
|
|
is_default BOOLEAN DEFAULT FALSE,
|
|
|
|
-- Status
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
version INTEGER DEFAULT 1,
|
|
|
|
-- Audit
|
|
created_by UUID,
|
|
updated_by UUID,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- TABLE: checklist_item_templates (Items de plantilla)
|
|
-- =============================================================================
|
|
CREATE TABLE field_service.checklist_item_templates (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
checklist_id UUID NOT NULL REFERENCES field_service.service_checklists(id) ON DELETE CASCADE,
|
|
|
|
-- Item definition
|
|
item_order INTEGER NOT NULL DEFAULT 0,
|
|
text VARCHAR(500) NOT NULL,
|
|
item_type field_service.checklist_item_type NOT NULL DEFAULT 'BOOLEAN',
|
|
|
|
-- Configuration
|
|
is_required BOOLEAN DEFAULT FALSE,
|
|
options JSONB, -- Para tipo SELECT: ["Opción 1", "Opción 2"]
|
|
unit VARCHAR(20), -- Para tipo NUMBER: 'km', 'hrs', 'psi', etc.
|
|
min_value DECIMAL(12,2), -- Validación para NUMBER
|
|
max_value DECIMAL(12,2),
|
|
|
|
-- Help
|
|
help_text VARCHAR(500),
|
|
photo_required BOOLEAN DEFAULT FALSE, -- Requiere foto adicional
|
|
|
|
-- Grouping
|
|
section VARCHAR(100), -- Sección del checklist
|
|
|
|
-- Status
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- TABLE: checklist_responses (Respuestas de checklist en un servicio)
|
|
-- =============================================================================
|
|
CREATE TABLE field_service.checklist_responses (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
-- References
|
|
incident_id UUID, -- Incidente/orden de servicio
|
|
service_order_id UUID, -- Orden de servicio alternativa
|
|
checklist_id UUID NOT NULL REFERENCES field_service.service_checklists(id),
|
|
technician_id UUID NOT NULL,
|
|
|
|
-- Status
|
|
status field_service.checklist_status DEFAULT 'STARTED',
|
|
|
|
-- Timing
|
|
started_at TIMESTAMPTZ DEFAULT NOW(),
|
|
completed_at TIMESTAMPTZ,
|
|
|
|
-- Location at start/end
|
|
start_latitude DECIMAL(10,7),
|
|
start_longitude DECIMAL(10,7),
|
|
end_latitude DECIMAL(10,7),
|
|
end_longitude DECIMAL(10,7),
|
|
|
|
-- Offline handling
|
|
is_offline BOOLEAN DEFAULT FALSE,
|
|
device_id VARCHAR(100),
|
|
local_id VARCHAR(100), -- ID local del dispositivo
|
|
synced_at TIMESTAMPTZ,
|
|
|
|
-- Summary
|
|
total_items INTEGER DEFAULT 0,
|
|
completed_items INTEGER DEFAULT 0,
|
|
passed_items INTEGER DEFAULT 0,
|
|
failed_items INTEGER DEFAULT 0,
|
|
|
|
-- Notes
|
|
notes TEXT,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- TABLE: checklist_item_responses (Respuestas individuales)
|
|
-- =============================================================================
|
|
CREATE TABLE field_service.checklist_item_responses (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
checklist_response_id UUID NOT NULL REFERENCES field_service.checklist_responses(id) ON DELETE CASCADE,
|
|
item_template_id UUID NOT NULL REFERENCES field_service.checklist_item_templates(id),
|
|
|
|
-- Response value
|
|
value_boolean BOOLEAN,
|
|
value_text TEXT,
|
|
value_number DECIMAL(12,4),
|
|
value_json JSONB, -- Para SELECT múltiple o datos complejos
|
|
|
|
-- Evidence
|
|
photo_url VARCHAR(500),
|
|
photo_urls JSONB, -- Múltiples fotos
|
|
signature_url VARCHAR(500),
|
|
|
|
-- Status
|
|
is_passed BOOLEAN, -- Pasó la verificación
|
|
failure_reason TEXT,
|
|
|
|
-- Timing
|
|
captured_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
-- Offline
|
|
local_id VARCHAR(100),
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- TABLE: work_logs (Registro de mano de obra)
|
|
-- =============================================================================
|
|
CREATE TABLE field_service.work_logs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
-- References
|
|
incident_id UUID,
|
|
service_order_id UUID,
|
|
technician_id UUID NOT NULL,
|
|
|
|
-- Activity
|
|
activity_code VARCHAR(50) NOT NULL, -- Código de catálogo
|
|
activity_name VARCHAR(200),
|
|
description TEXT,
|
|
|
|
-- Timing
|
|
start_time TIMESTAMPTZ NOT NULL,
|
|
end_time TIMESTAMPTZ,
|
|
duration_minutes INTEGER,
|
|
|
|
-- Pause tracking
|
|
pause_start TIMESTAMPTZ,
|
|
total_pause_minutes INTEGER DEFAULT 0,
|
|
|
|
-- Status
|
|
status field_service.worklog_status DEFAULT 'ACTIVE',
|
|
|
|
-- Labor calculation
|
|
labor_rate DECIMAL(10,2), -- Tarifa por hora
|
|
labor_total DECIMAL(12,2), -- Total calculado
|
|
is_overtime BOOLEAN DEFAULT FALSE,
|
|
overtime_multiplier DECIMAL(3,2) DEFAULT 1.5,
|
|
|
|
-- Location
|
|
latitude DECIMAL(10,7),
|
|
longitude DECIMAL(10,7),
|
|
|
|
-- Offline
|
|
is_offline BOOLEAN DEFAULT FALSE,
|
|
device_id VARCHAR(100),
|
|
local_id VARCHAR(100),
|
|
synced_at TIMESTAMPTZ,
|
|
|
|
-- Notes
|
|
notes TEXT,
|
|
|
|
-- Audit
|
|
created_by UUID,
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- TABLE: activity_catalog (Catálogo de actividades)
|
|
-- =============================================================================
|
|
CREATE TABLE field_service.activity_catalog (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
-- Activity definition
|
|
code VARCHAR(50) NOT NULL,
|
|
name VARCHAR(200) NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Categorization
|
|
category VARCHAR(100),
|
|
service_type_code VARCHAR(50),
|
|
|
|
-- Defaults
|
|
default_duration_minutes INTEGER,
|
|
default_rate DECIMAL(10,2),
|
|
|
|
-- Status
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
CONSTRAINT uq_activity_tenant_code UNIQUE (tenant_id, code)
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- TABLE: diagnosis_records (Registro de diagnóstico)
|
|
-- =============================================================================
|
|
CREATE TABLE field_service.diagnosis_records (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
-- References
|
|
incident_id UUID,
|
|
service_order_id UUID,
|
|
vehicle_id UUID,
|
|
technician_id UUID NOT NULL,
|
|
|
|
-- Diagnosis type
|
|
diagnosis_type field_service.diagnosis_type NOT NULL DEFAULT 'VISUAL',
|
|
|
|
-- Symptoms
|
|
symptoms TEXT NOT NULL,
|
|
customer_complaint TEXT,
|
|
|
|
-- Root cause
|
|
root_cause_code VARCHAR(50), -- Catálogo de causas raíz
|
|
root_cause_category VARCHAR(100),
|
|
root_cause_description TEXT,
|
|
|
|
-- OBD-II (for vehicles)
|
|
obd2_codes JSONB, -- ["P0300", "P0420", ...]
|
|
obd2_raw_data JSONB, -- Datos crudos del escáner
|
|
|
|
-- Readings
|
|
odometer_reading DECIMAL(12,2),
|
|
engine_hours DECIMAL(10,2),
|
|
fuel_level INTEGER, -- Porcentaje
|
|
|
|
-- Recommendation
|
|
recommendation TEXT,
|
|
severity VARCHAR(20), -- LOW, MEDIUM, HIGH, CRITICAL
|
|
requires_immediate_action BOOLEAN DEFAULT FALSE,
|
|
|
|
-- Evidence
|
|
photo_urls JSONB,
|
|
video_urls JSONB,
|
|
|
|
-- Location
|
|
latitude DECIMAL(10,7),
|
|
longitude DECIMAL(10,7),
|
|
|
|
-- Offline
|
|
is_offline BOOLEAN DEFAULT FALSE,
|
|
device_id VARCHAR(100),
|
|
local_id VARCHAR(100),
|
|
synced_at TIMESTAMPTZ,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- TABLE: root_cause_catalog (Catálogo de causas raíz)
|
|
-- =============================================================================
|
|
CREATE TABLE field_service.root_cause_catalog (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
-- Definition
|
|
code VARCHAR(50) NOT NULL,
|
|
name VARCHAR(200) NOT NULL,
|
|
description TEXT,
|
|
|
|
-- Categorization
|
|
category VARCHAR(100) NOT NULL,
|
|
subcategory VARCHAR(100),
|
|
|
|
-- Common for vehicle type
|
|
vehicle_types JSONB, -- ["Diesel", "Gasolina", "Híbrido"]
|
|
|
|
-- Recommendations
|
|
standard_recommendation TEXT,
|
|
estimated_repair_hours DECIMAL(5,2),
|
|
|
|
-- Status
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
CONSTRAINT uq_root_cause_tenant_code UNIQUE (tenant_id, code)
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- TABLE: field_evidence (Evidencias de campo: fotos, videos, firmas)
|
|
-- =============================================================================
|
|
CREATE TABLE field_service.field_evidence (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
-- References
|
|
incident_id UUID,
|
|
service_order_id UUID,
|
|
checklist_response_id UUID,
|
|
diagnosis_record_id UUID,
|
|
|
|
-- Evidence type
|
|
evidence_type VARCHAR(20) NOT NULL, -- PHOTO, VIDEO, SIGNATURE, DOCUMENT
|
|
|
|
-- File info
|
|
file_url VARCHAR(500) NOT NULL,
|
|
file_name VARCHAR(200),
|
|
file_size INTEGER,
|
|
mime_type VARCHAR(100),
|
|
|
|
-- Metadata
|
|
caption TEXT,
|
|
tags JSONB,
|
|
|
|
-- Photo specific
|
|
is_before BOOLEAN, -- Foto antes del servicio
|
|
is_after BOOLEAN, -- Foto después del servicio
|
|
|
|
-- Signature specific
|
|
signer_name VARCHAR(150),
|
|
signer_role VARCHAR(50), -- CUSTOMER, TECHNICIAN, SUPERVISOR
|
|
|
|
-- Location
|
|
latitude DECIMAL(10,7),
|
|
longitude DECIMAL(10,7),
|
|
|
|
-- Offline
|
|
is_offline BOOLEAN DEFAULT FALSE,
|
|
device_id VARCHAR(100),
|
|
local_id VARCHAR(100),
|
|
synced_at TIMESTAMPTZ,
|
|
pending_upload BOOLEAN DEFAULT FALSE,
|
|
|
|
-- Audit
|
|
captured_by UUID,
|
|
captured_at TIMESTAMPTZ DEFAULT NOW(),
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- TABLE: offline_queue_items (Cola de sincronización offline)
|
|
-- =============================================================================
|
|
CREATE TABLE field_service.offline_queue_items (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
|
|
-- Device
|
|
device_id VARCHAR(100) NOT NULL,
|
|
technician_id UUID,
|
|
|
|
-- Entity reference
|
|
entity_type VARCHAR(50) NOT NULL, -- ChecklistResponse, WorkLog, DiagnosisRecord, FieldEvidence
|
|
entity_id VARCHAR(100) NOT NULL, -- UUID o ID local
|
|
local_id VARCHAR(100) NOT NULL,
|
|
|
|
-- Payload
|
|
payload JSONB NOT NULL,
|
|
|
|
-- Sync status
|
|
status field_service.sync_status DEFAULT 'PENDING',
|
|
priority INTEGER DEFAULT 0, -- Mayor = más prioritario
|
|
|
|
-- Attempts
|
|
sync_attempts INTEGER DEFAULT 0,
|
|
max_attempts INTEGER DEFAULT 5,
|
|
last_attempt_at TIMESTAMPTZ,
|
|
next_attempt_at TIMESTAMPTZ,
|
|
|
|
-- Result
|
|
synced_at TIMESTAMPTZ,
|
|
server_entity_id UUID, -- ID asignado por el servidor
|
|
error_message TEXT,
|
|
error_code VARCHAR(50),
|
|
|
|
-- Conflict resolution
|
|
has_conflict BOOLEAN DEFAULT FALSE,
|
|
conflict_data JSONB,
|
|
resolved_at TIMESTAMPTZ,
|
|
resolution VARCHAR(20), -- LOCAL, SERVER, MERGED
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
CONSTRAINT uq_offline_device_local_id UNIQUE (device_id, local_id)
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- TABLE: field_checkins (Check-in/Check-out de técnicos)
|
|
-- =============================================================================
|
|
CREATE TABLE field_service.field_checkins (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL,
|
|
|
|
-- References
|
|
incident_id UUID,
|
|
service_order_id UUID,
|
|
technician_id UUID NOT NULL,
|
|
unit_id UUID, -- Unidad móvil
|
|
|
|
-- Checkin
|
|
checkin_time TIMESTAMPTZ NOT NULL,
|
|
checkin_latitude DECIMAL(10,7),
|
|
checkin_longitude DECIMAL(10,7),
|
|
checkin_address TEXT,
|
|
checkin_photo_url VARCHAR(500),
|
|
|
|
-- Checkout
|
|
checkout_time TIMESTAMPTZ,
|
|
checkout_latitude DECIMAL(10,7),
|
|
checkout_longitude DECIMAL(10,7),
|
|
checkout_photo_url VARCHAR(500),
|
|
|
|
-- Duration
|
|
on_site_minutes INTEGER,
|
|
|
|
-- Notes
|
|
notes TEXT,
|
|
|
|
-- Offline
|
|
is_offline BOOLEAN DEFAULT FALSE,
|
|
device_id VARCHAR(100),
|
|
synced_at TIMESTAMPTZ,
|
|
|
|
-- Audit
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- =============================================================================
|
|
-- INDEXES
|
|
-- =============================================================================
|
|
|
|
-- service_checklists
|
|
CREATE INDEX idx_checklists_tenant ON field_service.service_checklists(tenant_id);
|
|
CREATE INDEX idx_checklists_service_type ON field_service.service_checklists(tenant_id, service_type_code);
|
|
CREATE INDEX idx_checklists_active ON field_service.service_checklists(tenant_id, is_active);
|
|
|
|
-- checklist_item_templates
|
|
CREATE INDEX idx_checklist_items_checklist ON field_service.checklist_item_templates(checklist_id);
|
|
CREATE INDEX idx_checklist_items_order ON field_service.checklist_item_templates(checklist_id, item_order);
|
|
|
|
-- checklist_responses
|
|
CREATE INDEX idx_checklist_resp_tenant ON field_service.checklist_responses(tenant_id);
|
|
CREATE INDEX idx_checklist_resp_incident ON field_service.checklist_responses(tenant_id, incident_id);
|
|
CREATE INDEX idx_checklist_resp_technician ON field_service.checklist_responses(tenant_id, technician_id);
|
|
CREATE INDEX idx_checklist_resp_status ON field_service.checklist_responses(tenant_id, status);
|
|
CREATE INDEX idx_checklist_resp_offline ON field_service.checklist_responses(is_offline, synced_at) WHERE is_offline = TRUE;
|
|
|
|
-- checklist_item_responses
|
|
CREATE INDEX idx_item_resp_response ON field_service.checklist_item_responses(checklist_response_id);
|
|
|
|
-- work_logs
|
|
CREATE INDEX idx_worklogs_tenant ON field_service.work_logs(tenant_id);
|
|
CREATE INDEX idx_worklogs_incident ON field_service.work_logs(tenant_id, incident_id);
|
|
CREATE INDEX idx_worklogs_technician ON field_service.work_logs(tenant_id, technician_id);
|
|
CREATE INDEX idx_worklogs_active ON field_service.work_logs(tenant_id, status) WHERE status = 'ACTIVE';
|
|
CREATE INDEX idx_worklogs_date ON field_service.work_logs(tenant_id, start_time);
|
|
|
|
-- activity_catalog
|
|
CREATE INDEX idx_activity_tenant ON field_service.activity_catalog(tenant_id);
|
|
CREATE INDEX idx_activity_code ON field_service.activity_catalog(tenant_id, code);
|
|
|
|
-- diagnosis_records
|
|
CREATE INDEX idx_diagnosis_tenant ON field_service.diagnosis_records(tenant_id);
|
|
CREATE INDEX idx_diagnosis_incident ON field_service.diagnosis_records(tenant_id, incident_id);
|
|
CREATE INDEX idx_diagnosis_vehicle ON field_service.diagnosis_records(tenant_id, vehicle_id);
|
|
CREATE INDEX idx_diagnosis_technician ON field_service.diagnosis_records(tenant_id, technician_id);
|
|
CREATE INDEX idx_diagnosis_date ON field_service.diagnosis_records(tenant_id, created_at);
|
|
|
|
-- root_cause_catalog
|
|
CREATE INDEX idx_root_cause_tenant ON field_service.root_cause_catalog(tenant_id);
|
|
CREATE INDEX idx_root_cause_category ON field_service.root_cause_catalog(tenant_id, category);
|
|
|
|
-- field_evidence
|
|
CREATE INDEX idx_evidence_tenant ON field_service.field_evidence(tenant_id);
|
|
CREATE INDEX idx_evidence_incident ON field_service.field_evidence(tenant_id, incident_id);
|
|
CREATE INDEX idx_evidence_type ON field_service.field_evidence(tenant_id, evidence_type);
|
|
CREATE INDEX idx_evidence_pending ON field_service.field_evidence(pending_upload) WHERE pending_upload = TRUE;
|
|
|
|
-- offline_queue_items
|
|
CREATE INDEX idx_offline_queue_device ON field_service.offline_queue_items(device_id);
|
|
CREATE INDEX idx_offline_queue_status ON field_service.offline_queue_items(status);
|
|
CREATE INDEX idx_offline_queue_pending ON field_service.offline_queue_items(status, next_attempt_at) WHERE status = 'PENDING';
|
|
CREATE INDEX idx_offline_queue_entity ON field_service.offline_queue_items(entity_type, entity_id);
|
|
|
|
-- field_checkins
|
|
CREATE INDEX idx_checkins_tenant ON field_service.field_checkins(tenant_id);
|
|
CREATE INDEX idx_checkins_incident ON field_service.field_checkins(tenant_id, incident_id);
|
|
CREATE INDEX idx_checkins_technician ON field_service.field_checkins(tenant_id, technician_id);
|
|
CREATE INDEX idx_checkins_date ON field_service.field_checkins(tenant_id, checkin_time);
|
|
|
|
-- =============================================================================
|
|
-- TRIGGERS
|
|
-- =============================================================================
|
|
|
|
-- Auto-update timestamps
|
|
CREATE TRIGGER update_service_checklists_updated_at
|
|
BEFORE UPDATE ON field_service.service_checklists
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_checklist_item_templates_updated_at
|
|
BEFORE UPDATE ON field_service.checklist_item_templates
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_checklist_responses_updated_at
|
|
BEFORE UPDATE ON field_service.checklist_responses
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_work_logs_updated_at
|
|
BEFORE UPDATE ON field_service.work_logs
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_activity_catalog_updated_at
|
|
BEFORE UPDATE ON field_service.activity_catalog
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_diagnosis_records_updated_at
|
|
BEFORE UPDATE ON field_service.diagnosis_records
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_root_cause_catalog_updated_at
|
|
BEFORE UPDATE ON field_service.root_cause_catalog
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_field_checkins_updated_at
|
|
BEFORE UPDATE ON field_service.field_checkins
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
CREATE TRIGGER update_offline_queue_items_updated_at
|
|
BEFORE UPDATE ON field_service.offline_queue_items
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
|
|
|
-- =============================================================================
|
|
-- FUNCTIONS
|
|
-- =============================================================================
|
|
|
|
-- Calculate work log duration on stop
|
|
CREATE OR REPLACE FUNCTION field_service.calculate_worklog_duration()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF NEW.end_time IS NOT NULL AND OLD.end_time IS NULL THEN
|
|
NEW.duration_minutes := EXTRACT(EPOCH FROM (NEW.end_time - NEW.start_time)) / 60 - COALESCE(NEW.total_pause_minutes, 0);
|
|
IF NEW.labor_rate IS NOT NULL THEN
|
|
NEW.labor_total := (NEW.duration_minutes / 60.0) * NEW.labor_rate;
|
|
IF NEW.is_overtime THEN
|
|
NEW.labor_total := NEW.labor_total * NEW.overtime_multiplier;
|
|
END IF;
|
|
END IF;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_calculate_worklog_duration
|
|
BEFORE UPDATE ON field_service.work_logs
|
|
FOR EACH ROW EXECUTE FUNCTION field_service.calculate_worklog_duration();
|
|
|
|
-- Update checklist response counts
|
|
CREATE OR REPLACE FUNCTION field_service.update_checklist_counts()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
UPDATE field_service.checklist_responses
|
|
SET
|
|
completed_items = (
|
|
SELECT COUNT(*) FROM field_service.checklist_item_responses
|
|
WHERE checklist_response_id = COALESCE(NEW.checklist_response_id, OLD.checklist_response_id)
|
|
),
|
|
passed_items = (
|
|
SELECT COUNT(*) FROM field_service.checklist_item_responses
|
|
WHERE checklist_response_id = COALESCE(NEW.checklist_response_id, OLD.checklist_response_id)
|
|
AND is_passed = TRUE
|
|
),
|
|
failed_items = (
|
|
SELECT COUNT(*) FROM field_service.checklist_item_responses
|
|
WHERE checklist_response_id = COALESCE(NEW.checklist_response_id, OLD.checklist_response_id)
|
|
AND is_passed = FALSE
|
|
)
|
|
WHERE id = COALESCE(NEW.checklist_response_id, OLD.checklist_response_id);
|
|
|
|
RETURN COALESCE(NEW, OLD);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_update_checklist_counts
|
|
AFTER INSERT OR UPDATE OR DELETE ON field_service.checklist_item_responses
|
|
FOR EACH ROW EXECUTE FUNCTION field_service.update_checklist_counts();
|
|
|
|
-- Calculate on-site time for checkins
|
|
CREATE OR REPLACE FUNCTION field_service.calculate_onsite_time()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF NEW.checkout_time IS NOT NULL AND OLD.checkout_time IS NULL THEN
|
|
NEW.on_site_minutes := EXTRACT(EPOCH FROM (NEW.checkout_time - NEW.checkin_time)) / 60;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
CREATE TRIGGER trg_calculate_onsite_time
|
|
BEFORE UPDATE ON field_service.field_checkins
|
|
FOR EACH ROW EXECUTE FUNCTION field_service.calculate_onsite_time();
|
|
|
|
-- =============================================================================
|
|
-- VIEWS
|
|
-- =============================================================================
|
|
|
|
-- Active work logs
|
|
CREATE OR REPLACE VIEW field_service.vw_active_work_logs AS
|
|
SELECT
|
|
wl.id,
|
|
wl.tenant_id,
|
|
wl.technician_id,
|
|
wl.incident_id,
|
|
wl.service_order_id,
|
|
wl.activity_code,
|
|
wl.activity_name,
|
|
wl.start_time,
|
|
EXTRACT(EPOCH FROM (NOW() - wl.start_time)) / 60 AS elapsed_minutes,
|
|
wl.labor_rate,
|
|
wl.is_overtime
|
|
FROM field_service.work_logs wl
|
|
WHERE wl.status = 'ACTIVE';
|
|
|
|
-- Pending offline sync
|
|
CREATE OR REPLACE VIEW field_service.vw_pending_sync AS
|
|
SELECT
|
|
oq.id,
|
|
oq.device_id,
|
|
oq.technician_id,
|
|
oq.entity_type,
|
|
oq.local_id,
|
|
oq.status,
|
|
oq.sync_attempts,
|
|
oq.last_attempt_at,
|
|
oq.error_message,
|
|
oq.created_at
|
|
FROM field_service.offline_queue_items oq
|
|
WHERE oq.status IN ('PENDING', 'FAILED')
|
|
ORDER BY oq.priority DESC, oq.created_at ASC;
|
|
|
|
-- Technician field activity summary
|
|
CREATE OR REPLACE VIEW field_service.vw_technician_activity AS
|
|
SELECT
|
|
fc.tenant_id,
|
|
fc.technician_id,
|
|
DATE(fc.checkin_time) AS work_date,
|
|
COUNT(DISTINCT fc.id) AS checkins_count,
|
|
SUM(fc.on_site_minutes) AS total_onsite_minutes,
|
|
COUNT(DISTINCT cr.id) AS checklists_completed,
|
|
COUNT(DISTINCT dr.id) AS diagnoses_created,
|
|
SUM(wl.duration_minutes) AS total_labor_minutes
|
|
FROM field_service.field_checkins fc
|
|
LEFT JOIN field_service.checklist_responses cr
|
|
ON cr.technician_id = fc.technician_id
|
|
AND DATE(cr.completed_at) = DATE(fc.checkin_time)
|
|
AND cr.status = 'COMPLETED'
|
|
LEFT JOIN field_service.diagnosis_records dr
|
|
ON dr.technician_id = fc.technician_id
|
|
AND DATE(dr.created_at) = DATE(fc.checkin_time)
|
|
LEFT JOIN field_service.work_logs wl
|
|
ON wl.technician_id = fc.technician_id
|
|
AND DATE(wl.start_time) = DATE(fc.checkin_time)
|
|
AND wl.status = 'COMPLETED'
|
|
GROUP BY fc.tenant_id, fc.technician_id, DATE(fc.checkin_time);
|
|
|
|
-- Checklist completion summary
|
|
CREATE OR REPLACE VIEW field_service.vw_checklist_summary AS
|
|
SELECT
|
|
cr.tenant_id,
|
|
cr.checklist_id,
|
|
sc.name AS checklist_name,
|
|
COUNT(*) AS total_responses,
|
|
COUNT(*) FILTER (WHERE cr.status = 'COMPLETED') AS completed_count,
|
|
AVG(cr.completed_items::FLOAT / NULLIF(cr.total_items, 0) * 100) AS avg_completion_rate,
|
|
AVG(EXTRACT(EPOCH FROM (cr.completed_at - cr.started_at)) / 60) FILTER (WHERE cr.status = 'COMPLETED') AS avg_completion_minutes
|
|
FROM field_service.checklist_responses cr
|
|
JOIN field_service.service_checklists sc ON sc.id = cr.checklist_id
|
|
GROUP BY cr.tenant_id, cr.checklist_id, sc.name;
|
|
|
|
-- =============================================================================
|
|
-- RLS POLICIES
|
|
-- =============================================================================
|
|
|
|
-- Enable RLS on all tables
|
|
ALTER TABLE field_service.service_checklists ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE field_service.checklist_item_templates ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE field_service.checklist_responses ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE field_service.checklist_item_responses ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE field_service.work_logs ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE field_service.activity_catalog ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE field_service.diagnosis_records ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE field_service.root_cause_catalog ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE field_service.field_evidence ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE field_service.field_checkins ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- RLS Policies
|
|
CREATE POLICY tenant_isolation_service_checklists ON field_service.service_checklists
|
|
FOR ALL USING (tenant_id = current_setting('app.tenant_id')::UUID);
|
|
|
|
CREATE POLICY tenant_isolation_checklist_item_templates ON field_service.checklist_item_templates
|
|
FOR ALL USING (tenant_id = current_setting('app.tenant_id')::UUID);
|
|
|
|
CREATE POLICY tenant_isolation_checklist_responses ON field_service.checklist_responses
|
|
FOR ALL USING (tenant_id = current_setting('app.tenant_id')::UUID);
|
|
|
|
CREATE POLICY tenant_isolation_checklist_item_responses ON field_service.checklist_item_responses
|
|
FOR ALL USING (tenant_id = current_setting('app.tenant_id')::UUID);
|
|
|
|
CREATE POLICY tenant_isolation_work_logs ON field_service.work_logs
|
|
FOR ALL USING (tenant_id = current_setting('app.tenant_id')::UUID);
|
|
|
|
CREATE POLICY tenant_isolation_activity_catalog ON field_service.activity_catalog
|
|
FOR ALL USING (tenant_id = current_setting('app.tenant_id')::UUID);
|
|
|
|
CREATE POLICY tenant_isolation_diagnosis_records ON field_service.diagnosis_records
|
|
FOR ALL USING (tenant_id = current_setting('app.tenant_id')::UUID);
|
|
|
|
CREATE POLICY tenant_isolation_root_cause_catalog ON field_service.root_cause_catalog
|
|
FOR ALL USING (tenant_id = current_setting('app.tenant_id')::UUID);
|
|
|
|
CREATE POLICY tenant_isolation_field_evidence ON field_service.field_evidence
|
|
FOR ALL USING (tenant_id = current_setting('app.tenant_id')::UUID);
|
|
|
|
CREATE POLICY tenant_isolation_field_checkins ON field_service.field_checkins
|
|
FOR ALL USING (tenant_id = current_setting('app.tenant_id')::UUID);
|
|
|
|
-- Note: offline_queue_items no tiene RLS ya que es por device, no por tenant
|
|
|
|
-- =============================================================================
|
|
-- COMMENTS
|
|
-- =============================================================================
|
|
|
|
COMMENT ON SCHEMA field_service IS 'MMD-012: Field Service / Bitácora Técnica Móvil';
|
|
COMMENT ON TABLE field_service.service_checklists IS 'Plantillas de checklist por tipo de servicio';
|
|
COMMENT ON TABLE field_service.checklist_item_templates IS 'Items de las plantillas de checklist';
|
|
COMMENT ON TABLE field_service.checklist_responses IS 'Respuestas de checklist capturadas en servicio';
|
|
COMMENT ON TABLE field_service.checklist_item_responses IS 'Respuestas individuales por item';
|
|
COMMENT ON TABLE field_service.work_logs IS 'Registro de mano de obra (timer)';
|
|
COMMENT ON TABLE field_service.activity_catalog IS 'Catálogo de actividades de mano de obra';
|
|
COMMENT ON TABLE field_service.diagnosis_records IS 'Registro de diagnósticos y causas raíz';
|
|
COMMENT ON TABLE field_service.root_cause_catalog IS 'Catálogo de causas raíz';
|
|
COMMENT ON TABLE field_service.field_evidence IS 'Evidencias: fotos, videos, firmas';
|
|
COMMENT ON TABLE field_service.offline_queue_items IS 'Cola de sincronización offline';
|
|
COMMENT ON TABLE field_service.field_checkins IS 'Check-in/Check-out de técnicos en sitio';
|
|
|
|
-- =============================================================================
|
|
-- END OF SCHEMA
|
|
-- =============================================================================
|