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:
Adrian Flores Cortes 2026-01-27 02:42:50 -06:00
parent a746fdcd31
commit 9902b4bbe1

View 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
-- =============================================================================