From d72145f0b746bcd7fcbe06b66277378aa364f845 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Tue, 27 Jan 2026 01:33:57 -0600 Subject: [PATCH] feat(MMD-014): Add GPS tracking schema - Create gps_tracking schema with 5 tables - Tables: gps_devices, gps_positions, geofences, geofence_events, route_segments - Add PostGIS-compatible distance functions (haversine_distance) - Add RLS policies for multi-tenant isolation - Add views: vw_last_positions, vw_device_summary - Add triggers for automatic last_position updates Co-Authored-By: Claude Opus 4.5 --- init/14-gps-tracking-schema.sql | 530 ++++++++++++++++++++++++++++++++ 1 file changed, 530 insertions(+) create mode 100644 init/14-gps-tracking-schema.sql diff --git a/init/14-gps-tracking-schema.sql b/init/14-gps-tracking-schema.sql new file mode 100644 index 0000000..dc1937a --- /dev/null +++ b/init/14-gps-tracking-schema.sql @@ -0,0 +1,530 @@ +-- =========================================== +-- MECANICAS DIESEL - Schema gps_tracking +-- =========================================== +-- Integracion GPS, posiciones, geocercas, rutas +-- Modulo: MMD-014 GPS Integration + +-- ------------------------------------------- +-- SCHEMA CREATION +-- ------------------------------------------- +CREATE SCHEMA IF NOT EXISTS gps_tracking; +COMMENT ON SCHEMA gps_tracking IS 'Tracking GPS, posiciones en tiempo real, geocercas y calculo de rutas'; + +GRANT USAGE ON SCHEMA gps_tracking TO mecanicas_user; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA gps_tracking TO mecanicas_user; +ALTER DEFAULT PRIVILEGES IN SCHEMA gps_tracking GRANT ALL ON TABLES TO mecanicas_user; + +SET search_path TO gps_tracking, public; + +-- ------------------------------------------- +-- GPS_DEVICES - Dispositivos GPS vinculados a unidades +-- ------------------------------------------- +CREATE TABLE gps_tracking.gps_devices ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + + -- Vinculacion a unidad de flota + unit_id UUID NOT NULL, -- FK a vehicle_management.vehicles + unit_type VARCHAR(20) DEFAULT 'vehicle' + CHECK (unit_type IN ('vehicle', 'trailer', 'equipment', 'technician')), + + -- Identificacion externa (plataforma GPS) + external_device_id VARCHAR(100) NOT NULL, + platform VARCHAR(30) NOT NULL DEFAULT 'traccar' + CHECK (platform IN ('traccar', 'wialon', 'samsara', 'geotab', 'manual')), + + -- Identificadores del dispositivo + imei VARCHAR(20), + serial_number VARCHAR(50), + phone_number VARCHAR(20), + model VARCHAR(50), + manufacturer VARCHAR(50), + + -- Estado + is_active BOOLEAN DEFAULT TRUE, + last_position_at TIMESTAMP WITH TIME ZONE, + last_position_lat DECIMAL(10, 7), + last_position_lng DECIMAL(10, 7), + + -- Configuracion + position_interval_seconds INTEGER DEFAULT 30 CHECK (position_interval_seconds >= 5), + metadata JSONB DEFAULT '{}', + + -- Auditoria + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_by UUID, + + CONSTRAINT uq_device_external UNIQUE (tenant_id, platform, external_device_id), + CONSTRAINT uq_device_unit UNIQUE (tenant_id, unit_id) +); + +CREATE INDEX idx_gps_devices_tenant ON gps_tracking.gps_devices(tenant_id); +CREATE INDEX idx_gps_devices_unit ON gps_tracking.gps_devices(unit_id); +CREATE INDEX idx_gps_devices_external ON gps_tracking.gps_devices(external_device_id); +CREATE INDEX idx_gps_devices_platform ON gps_tracking.gps_devices(platform); +CREATE INDEX idx_gps_devices_active ON gps_tracking.gps_devices(is_active) WHERE is_active = TRUE; + +CREATE TRIGGER trg_gps_devices_updated_at + BEFORE UPDATE ON gps_tracking.gps_devices + FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at(); + +SELECT create_tenant_rls_policies('gps_tracking', 'gps_devices'); + +COMMENT ON TABLE gps_tracking.gps_devices IS 'Dispositivos GPS vinculados a unidades de la flota'; +COMMENT ON COLUMN gps_tracking.gps_devices.external_device_id IS 'ID del dispositivo en la plataforma GPS externa (Traccar, etc.)'; +COMMENT ON COLUMN gps_tracking.gps_devices.platform IS 'Plataforma GPS: traccar (recomendado), wialon, samsara, geotab, manual'; + +-- ------------------------------------------- +-- GPS_POSITIONS - Posiciones registradas (time-series) +-- ------------------------------------------- +CREATE TABLE gps_tracking.gps_positions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + + device_id UUID NOT NULL REFERENCES gps_tracking.gps_devices(id) ON DELETE CASCADE, + unit_id UUID NOT NULL, -- Denormalizado para queries rapidos + + -- Posicion + latitude DECIMAL(10, 7) NOT NULL CHECK (latitude >= -90 AND latitude <= 90), + longitude DECIMAL(10, 7) NOT NULL CHECK (longitude >= -180 AND longitude <= 180), + altitude DECIMAL(8, 2), -- metros sobre nivel del mar + + -- Movimiento + speed DECIMAL(6, 2) CHECK (speed >= 0), -- km/h + course DECIMAL(5, 2) CHECK (course >= 0 AND course < 360), -- grados (0 = norte) + + -- Precision + accuracy DECIMAL(6, 2) CHECK (accuracy >= 0), -- metros + hdop DECIMAL(4, 2), -- Horizontal Dilution of Precision + + -- Atributos adicionales (del dispositivo) + attributes JSONB DEFAULT '{}', + -- attributes puede contener: ignition, fuel, odometer, engineHours, batteryLevel, etc. + + -- Timestamps + device_time TIMESTAMP WITH TIME ZONE NOT NULL, -- Hora del dispositivo + server_time TIMESTAMP WITH TIME ZONE DEFAULT NOW(), -- Hora de recepcion + fix_time TIMESTAMP WITH TIME ZONE, -- Hora del fix GPS + + -- Validacion + is_valid BOOLEAN DEFAULT TRUE, -- Si paso filtros anti-ruido + + -- Indices para busquedas + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Particionar por fecha si hay muchos registros +-- CREATE TABLE gps_tracking.gps_positions_YYYY_MM PARTITION OF gps_tracking.gps_positions +-- FOR VALUES FROM ('2026-01-01') TO ('2026-02-01'); + +CREATE INDEX idx_gps_positions_tenant ON gps_tracking.gps_positions(tenant_id); +CREATE INDEX idx_gps_positions_device ON gps_tracking.gps_positions(device_id); +CREATE INDEX idx_gps_positions_unit ON gps_tracking.gps_positions(unit_id); +CREATE INDEX idx_gps_positions_device_time ON gps_tracking.gps_positions(device_id, device_time DESC); +CREATE INDEX idx_gps_positions_time ON gps_tracking.gps_positions(device_time DESC); +CREATE INDEX idx_gps_positions_valid ON gps_tracking.gps_positions(is_valid) WHERE is_valid = TRUE; + +-- Indice espacial para queries geograficos +CREATE INDEX idx_gps_positions_location ON gps_tracking.gps_positions + USING GIST (ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)) + WHERE longitude IS NOT NULL AND latitude IS NOT NULL; + +SELECT create_tenant_rls_policies('gps_tracking', 'gps_positions'); + +COMMENT ON TABLE gps_tracking.gps_positions IS 'Posiciones GPS registradas - datos time-series'; +COMMENT ON COLUMN gps_tracking.gps_positions.attributes IS 'Atributos adicionales: ignition, fuel, odometer, engineHours, etc.'; +COMMENT ON COLUMN gps_tracking.gps_positions.is_valid IS 'Si la posicion paso los filtros anti-ruido'; + +-- ------------------------------------------- +-- GEOFENCES - Geocercas (areas de interes) +-- ------------------------------------------- +CREATE TABLE gps_tracking.geofences ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + + -- Identificacion + name VARCHAR(100) NOT NULL, + code VARCHAR(30), + description TEXT, + + -- Tipo de geocerca + geofence_type VARCHAR(20) NOT NULL DEFAULT 'circle' + CHECK (geofence_type IN ('circle', 'polygon')), + + -- Para tipo CIRCLE + center_lat DECIMAL(10, 7), + center_lng DECIMAL(10, 7), + radius_meters DECIMAL(10, 2) CHECK (radius_meters > 0), + + -- Para tipo POLYGON (formato GeoJSON) + polygon_geojson JSONB, + -- Ejemplo: {"type": "Polygon", "coordinates": [[[lng1,lat1],[lng2,lat2],...]]} + + -- Geometria PostGIS (calculada) + geometry GEOMETRY(Geometry, 4326), + + -- Categoria operativa + category VARCHAR(30) NOT NULL DEFAULT 'custom' + CHECK (category IN ('base', 'coverage', 'restricted', 'high_risk', 'client', 'custom')), + + -- Configuracion de eventos + trigger_on_enter BOOLEAN DEFAULT TRUE, + trigger_on_exit BOOLEAN DEFAULT TRUE, + dwell_time_seconds INTEGER DEFAULT 0, -- Tiempo minimo para disparar evento + + -- Estado + is_active BOOLEAN DEFAULT TRUE, + color VARCHAR(7) DEFAULT '#3388ff', -- Color en mapa (hex) + metadata JSONB DEFAULT '{}', + + -- Auditoria + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + created_by UUID +); + +CREATE INDEX idx_geofences_tenant ON gps_tracking.geofences(tenant_id); +CREATE INDEX idx_geofences_category ON gps_tracking.geofences(category); +CREATE INDEX idx_geofences_active ON gps_tracking.geofences(is_active) WHERE is_active = TRUE; + +-- Indice espacial +CREATE INDEX idx_geofences_geometry ON gps_tracking.geofences USING GIST (geometry); + +CREATE TRIGGER trg_geofences_updated_at + BEFORE UPDATE ON gps_tracking.geofences + FOR EACH ROW EXECUTE FUNCTION trigger_set_updated_at(); + +SELECT create_tenant_rls_policies('gps_tracking', 'geofences'); + +COMMENT ON TABLE gps_tracking.geofences IS 'Geocercas para detectar entrada/salida de areas'; +COMMENT ON COLUMN gps_tracking.geofences.category IS 'Categoria: base (taller), coverage (zona atendida), restricted, high_risk, client, custom'; +COMMENT ON COLUMN gps_tracking.geofences.dwell_time_seconds IS 'Segundos minimos dentro/fuera para disparar evento'; + +-- Trigger para calcular geometria automaticamente +CREATE OR REPLACE FUNCTION gps_tracking.calculate_geofence_geometry() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.geofence_type = 'circle' AND NEW.center_lat IS NOT NULL AND NEW.center_lng IS NOT NULL THEN + -- Crear circulo + NEW.geometry := ST_Buffer( + ST_SetSRID(ST_MakePoint(NEW.center_lng, NEW.center_lat), 4326)::geography, + NEW.radius_meters + )::geometry; + ELSIF NEW.geofence_type = 'polygon' AND NEW.polygon_geojson IS NOT NULL THEN + -- Crear poligono desde GeoJSON + NEW.geometry := ST_SetSRID(ST_GeomFromGeoJSON(NEW.polygon_geojson::text), 4326); + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_geofences_geometry + BEFORE INSERT OR UPDATE ON gps_tracking.geofences + FOR EACH ROW EXECUTE FUNCTION gps_tracking.calculate_geofence_geometry(); + +-- ------------------------------------------- +-- GEOFENCE_EVENTS - Eventos de entrada/salida +-- ------------------------------------------- +CREATE TABLE gps_tracking.geofence_events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + + geofence_id UUID NOT NULL REFERENCES gps_tracking.geofences(id) ON DELETE CASCADE, + device_id UUID NOT NULL REFERENCES gps_tracking.gps_devices(id) ON DELETE CASCADE, + unit_id UUID NOT NULL, + + -- Tipo de evento + event_type VARCHAR(10) NOT NULL CHECK (event_type IN ('enter', 'exit', 'dwell')), + + -- Posicion que disparo el evento + position_id UUID REFERENCES gps_tracking.gps_positions(id), + latitude DECIMAL(10, 7) NOT NULL, + longitude DECIMAL(10, 7) NOT NULL, + + -- Timestamps + event_time TIMESTAMP WITH TIME ZONE NOT NULL, + processed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + -- Vinculacion a operaciones + related_incident_id UUID, -- FK a rescue_order si aplica + + -- Metadata + metadata JSONB DEFAULT '{}' +); + +CREATE INDEX idx_geofence_events_tenant ON gps_tracking.geofence_events(tenant_id); +CREATE INDEX idx_geofence_events_geofence ON gps_tracking.geofence_events(geofence_id); +CREATE INDEX idx_geofence_events_device ON gps_tracking.geofence_events(device_id); +CREATE INDEX idx_geofence_events_unit ON gps_tracking.geofence_events(unit_id); +CREATE INDEX idx_geofence_events_time ON gps_tracking.geofence_events(event_time DESC); +CREATE INDEX idx_geofence_events_incident ON gps_tracking.geofence_events(related_incident_id) + WHERE related_incident_id IS NOT NULL; + +SELECT create_tenant_rls_policies('gps_tracking', 'geofence_events'); + +COMMENT ON TABLE gps_tracking.geofence_events IS 'Eventos de entrada/salida de geocercas'; + +-- ------------------------------------------- +-- ROUTE_SEGMENTS - Segmentos de ruta para km facturables +-- ------------------------------------------- +CREATE TABLE gps_tracking.route_segments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + + -- Vinculacion a incidente/servicio + incident_id UUID, -- FK a rescue_order + unit_id UUID NOT NULL, + device_id UUID REFERENCES gps_tracking.gps_devices(id), + + -- Posiciones de inicio/fin + start_position_id UUID REFERENCES gps_tracking.gps_positions(id), + end_position_id UUID REFERENCES gps_tracking.gps_positions(id), + + -- Coordenadas (denormalizadas) + start_lat DECIMAL(10, 7) NOT NULL, + start_lng DECIMAL(10, 7) NOT NULL, + end_lat DECIMAL(10, 7) NOT NULL, + end_lng DECIMAL(10, 7) NOT NULL, + + -- Distancias + distance_km DECIMAL(10, 3) NOT NULL CHECK (distance_km >= 0), + raw_distance_km DECIMAL(10, 3), -- Antes de filtros + + -- Tiempos + start_time TIMESTAMP WITH TIME ZONE NOT NULL, + end_time TIMESTAMP WITH TIME ZONE NOT NULL, + duration_minutes DECIMAL(8, 2) CHECK (duration_minutes >= 0), + + -- Tipo de segmento + segment_type VARCHAR(30) NOT NULL DEFAULT 'other' + CHECK (segment_type IN ('to_incident', 'at_incident', 'return', 'between_incidents', 'other')), + + -- Validacion + is_valid BOOLEAN DEFAULT TRUE, + validation_notes TEXT, + + -- Polyline codificada (para visualizacion) + encoded_polyline TEXT, + + -- Metadata + metadata JSONB DEFAULT '{}', + + -- Auditoria + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + calculated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + + CONSTRAINT chk_segment_times CHECK (end_time >= start_time) +); + +CREATE INDEX idx_route_segments_tenant ON gps_tracking.route_segments(tenant_id); +CREATE INDEX idx_route_segments_incident ON gps_tracking.route_segments(incident_id); +CREATE INDEX idx_route_segments_unit ON gps_tracking.route_segments(unit_id); +CREATE INDEX idx_route_segments_type ON gps_tracking.route_segments(segment_type); +CREATE INDEX idx_route_segments_time ON gps_tracking.route_segments(start_time DESC); +CREATE INDEX idx_route_segments_valid ON gps_tracking.route_segments(is_valid) WHERE is_valid = TRUE; + +SELECT create_tenant_rls_policies('gps_tracking', 'route_segments'); + +COMMENT ON TABLE gps_tracking.route_segments IS 'Segmentos de ruta calculados para facturacion de km'; +COMMENT ON COLUMN gps_tracking.route_segments.segment_type IS 'Tipo: to_incident (ida), at_incident (en sitio), return (regreso), between_incidents, other'; +COMMENT ON COLUMN gps_tracking.route_segments.is_valid IS 'Si el segmento paso validacion anti-ruido'; +COMMENT ON COLUMN gps_tracking.route_segments.encoded_polyline IS 'Polyline codificada (Google format) para visualizar en mapa'; + +-- ------------------------------------------- +-- FUNCTIONS: Calculo de distancias +-- ------------------------------------------- + +-- Funcion para calcular distancia entre dos puntos (Haversine) +CREATE OR REPLACE FUNCTION gps_tracking.haversine_distance( + lat1 DECIMAL, lng1 DECIMAL, + lat2 DECIMAL, lng2 DECIMAL +) +RETURNS DECIMAL AS $$ +DECLARE + R DECIMAL := 6371; -- Radio de la Tierra en km + dlat DECIMAL; + dlng DECIMAL; + a DECIMAL; + c DECIMAL; +BEGIN + dlat := RADIANS(lat2 - lat1); + dlng := RADIANS(lng2 - lng1); + + a := SIN(dlat/2) * SIN(dlat/2) + + COS(RADIANS(lat1)) * COS(RADIANS(lat2)) * + SIN(dlng/2) * SIN(dlng/2); + + c := 2 * ATAN2(SQRT(a), SQRT(1-a)); + + RETURN R * c; +END; +$$ LANGUAGE plpgsql IMMUTABLE; + +COMMENT ON FUNCTION gps_tracking.haversine_distance IS 'Calcula distancia en km entre dos puntos usando formula Haversine'; + +-- Funcion para calcular distancia total de una ruta (array de posiciones) +CREATE OR REPLACE FUNCTION gps_tracking.calculate_route_distance( + p_device_id UUID, + p_start_time TIMESTAMP WITH TIME ZONE, + p_end_time TIMESTAMP WITH TIME ZONE +) +RETURNS TABLE ( + total_distance_km DECIMAL, + position_count INTEGER, + duration_minutes DECIMAL +) AS $$ +DECLARE + v_prev_lat DECIMAL; + v_prev_lng DECIMAL; + v_prev_time TIMESTAMP WITH TIME ZONE; + v_distance DECIMAL := 0; + v_count INTEGER := 0; + v_rec RECORD; +BEGIN + FOR v_rec IN + SELECT latitude, longitude, device_time + FROM gps_tracking.gps_positions + WHERE device_id = p_device_id + AND device_time BETWEEN p_start_time AND p_end_time + AND is_valid = TRUE + ORDER BY device_time + LOOP + IF v_prev_lat IS NOT NULL THEN + v_distance := v_distance + gps_tracking.haversine_distance( + v_prev_lat, v_prev_lng, + v_rec.latitude, v_rec.longitude + ); + ELSE + v_prev_time := v_rec.device_time; + END IF; + + v_prev_lat := v_rec.latitude; + v_prev_lng := v_rec.longitude; + v_count := v_count + 1; + END LOOP; + + RETURN QUERY SELECT + ROUND(v_distance, 3)::DECIMAL, + v_count, + ROUND(EXTRACT(EPOCH FROM (v_rec.device_time - v_prev_time)) / 60, 2)::DECIMAL; +END; +$$ LANGUAGE plpgsql STABLE; + +COMMENT ON FUNCTION gps_tracking.calculate_route_distance IS 'Calcula distancia total de una ruta entre dos timestamps'; + +-- Funcion para verificar si un punto esta dentro de una geocerca +CREATE OR REPLACE FUNCTION gps_tracking.is_point_in_geofence( + p_lat DECIMAL, + p_lng DECIMAL, + p_geofence_id UUID +) +RETURNS BOOLEAN AS $$ +DECLARE + v_geofence gps_tracking.geofences; +BEGIN + SELECT * INTO v_geofence + FROM gps_tracking.geofences + WHERE id = p_geofence_id AND is_active = TRUE; + + IF v_geofence IS NULL THEN + RETURN FALSE; + END IF; + + -- Usar PostGIS para verificacion precisa + RETURN ST_Contains( + v_geofence.geometry, + ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326) + ); +END; +$$ LANGUAGE plpgsql STABLE; + +COMMENT ON FUNCTION gps_tracking.is_point_in_geofence IS 'Verifica si un punto esta dentro de una geocerca'; + +-- ------------------------------------------- +-- VIEWS +-- ------------------------------------------- + +-- Vista de ultima posicion por unidad +CREATE OR REPLACE VIEW gps_tracking.vw_last_positions AS +SELECT DISTINCT ON (d.unit_id) + d.id as device_id, + d.tenant_id, + d.unit_id, + d.external_device_id, + d.platform, + d.is_active, + p.id as position_id, + p.latitude, + p.longitude, + p.speed, + p.course, + p.attributes, + p.device_time, + p.is_valid, + EXTRACT(EPOCH FROM (NOW() - p.device_time)) / 60 as minutes_ago +FROM gps_tracking.gps_devices d +LEFT JOIN gps_tracking.gps_positions p ON d.id = p.device_id +WHERE d.is_active = TRUE + AND d.tenant_id = get_current_tenant_id() +ORDER BY d.unit_id, p.device_time DESC; + +COMMENT ON VIEW gps_tracking.vw_last_positions IS 'Vista de ultima posicion conocida por unidad con filtro RLS'; + +-- Vista de resumen de dispositivos +CREATE OR REPLACE VIEW gps_tracking.vw_device_summary AS +SELECT + d.id, + d.tenant_id, + d.unit_id, + d.external_device_id, + d.platform, + d.model, + d.is_active, + d.last_position_at, + d.last_position_lat, + d.last_position_lng, + CASE + WHEN d.last_position_at IS NULL THEN 'never' + WHEN NOW() - d.last_position_at < INTERVAL '5 minutes' THEN 'online' + WHEN NOW() - d.last_position_at < INTERVAL '1 hour' THEN 'recent' + ELSE 'offline' + END as connection_status, + (SELECT COUNT(*) FROM gps_tracking.gps_positions p + WHERE p.device_id = d.id AND p.device_time > NOW() - INTERVAL '24 hours') as positions_24h +FROM gps_tracking.gps_devices d +WHERE d.tenant_id = get_current_tenant_id(); + +COMMENT ON VIEW gps_tracking.vw_device_summary IS 'Vista resumida de dispositivos GPS con estado de conexion'; + +-- ------------------------------------------- +-- TRIGGER: Actualizar ultima posicion en device +-- ------------------------------------------- +CREATE OR REPLACE FUNCTION gps_tracking.update_device_last_position() +RETURNS TRIGGER AS $$ +BEGIN + UPDATE gps_tracking.gps_devices + SET + last_position_at = NEW.device_time, + last_position_lat = NEW.latitude, + last_position_lng = NEW.longitude + WHERE id = NEW.device_id + AND (last_position_at IS NULL OR last_position_at < NEW.device_time); + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_update_device_last_position + AFTER INSERT ON gps_tracking.gps_positions + FOR EACH ROW EXECUTE FUNCTION gps_tracking.update_device_last_position(); + +-- ------------------------------------------- +-- DATOS INICIALES: Geocercas base +-- ------------------------------------------- +-- NOTA: Ejecutar despues de tener tenant_id valido +-- INSERT INTO gps_tracking.geofences (tenant_id, name, code, geofence_type, center_lat, center_lng, radius_meters, category) +-- VALUES +-- ('{tenant_id}', 'Taller Principal', 'BASE-01', 'circle', 19.4326, -99.1332, 200, 'base'); + +COMMENT ON SCHEMA gps_tracking IS 'Schema completo para tracking GPS - MMD-014';