feat(MMD-012): Add field service schema
- 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>
This commit is contained in:
parent
a746fdcd31
commit
9902b4bbe1
839
init/17-field-service-schema.sql
Normal file
839
init/17-field-service-schema.sql
Normal file
@ -0,0 +1,839 @@
|
||||
-- =============================================================================
|
||||
-- 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
|
||||
-- =============================================================================
|
||||
Loading…
Reference in New Issue
Block a user