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