Commit 34c9927f00ba48166e752d0da2214d69d8e9d54a

Authored by Antigravity AI
1 parent 480b16a1

Ajuste de reglas y vista 505 para control visual

Showing 57 changed files with 1440 additions and 213 deletions
GIS-GEOSERVER-REGLAS.md
@@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
2 2
3 Regla 0. Eres un senior fullstack developer con experiencia reconocida en Base de Datos PostgreSQL, amplia experiencia en proyectos corporativos con geoserver con uso de plataformas basadas en Java, javascript, HTML, XML, git, jenkins, maven, springboot, swagger, proxy de apache y nginx. 3 Regla 0. Eres un senior fullstack developer con experiencia reconocida en Base de Datos PostgreSQL, amplia experiencia en proyectos corporativos con geoserver con uso de plataformas basadas en Java, javascript, HTML, XML, git, jenkins, maven, springboot, swagger, proxy de apache y nginx.
4 El ecosistema principal es Java 21 con Spring Boot y deberá usarse de forma preferencial todas las veces que se pueda. 4 El ecosistema principal es Java 21 con Spring Boot y deberá usarse de forma preferencial todas las veces que se pueda.
  5 +El frontend usa el framework corporativo AdminLTE/Bootstrap.
5 6
6 Regla 1. El ambiente de desarrollo y compilación se encuentra en el 192.168.1.123. 7 Regla 1. El ambiente de desarrollo y compilación se encuentra en el 192.168.1.123.
7 El JENKINS a usar está en este servidor. Los comandos del jenkins se ejecutan en el servidor 192.168.1.123. 8 El JENKINS a usar está en este servidor. Los comandos del jenkins se ejecutan en el servidor 192.168.1.123.
@@ -86,6 +87,7 @@ Regla 23. Columnas de Unión (Joins). Standard SNC. @@ -86,6 +87,7 @@ Regla 23. Columnas de Unión (Joins). Standard SNC.
86 Para toda vista de unión con el FDW, se debe utilizar la columna `snc_cuenta` (limpia) contra `REPLACE(liq.inm_ctacatastral, '-', '')`. 87 Para toda vista de unión con el FDW, se debe utilizar la columna `snc_cuenta` (limpia) contra `REPLACE(liq.inm_ctacatastral, '-', '')`.
87 La definición del SQL de JOIN entre los datos SNC (datos georreferenciados) y la base de datos del municipio (datos alfanuméricos) genera la vista `public.vw_lotes_morosidad_XXX` a usar para desplegar mapa georreferenciado coloreado y es la siguiente: 88 La definición del SQL de JOIN entre los datos SNC (datos georreferenciados) y la base de datos del municipio (datos alfanuméricos) genera la vista `public.vw_lotes_morosidad_XXX` a usar para desplegar mapa georreferenciado coloreado y es la siguiente:
88 ```sql 89 ```sql
  90 +DROP VIEW IF EXISTS public.vw_lotes_morosidad_XXX CASCADE;
89 CREATE OR REPLACE VIEW public.vw_lotes_morosidad_XXX AS 91 CREATE OR REPLACE VIEW public.vw_lotes_morosidad_XXX AS
90 SELECT 92 SELECT
91 lot.*, 93 lot.*,
@@ -114,13 +116,21 @@ Almacenamiento en carpeta cronológica dentro de /publico/. @@ -114,13 +116,21 @@ Almacenamiento en carpeta cronológica dentro de /publico/.
114 116
115 Solo bajo autorización del usuario. 117 Solo bajo autorización del usuario.
116 118
117 -Regla 26. Normalización Universal de Cartografía SNC.  
118 -El motor de importación debe aplicar una limpieza universal al campo snc_cuenta para asegurar el match tributario.  
119 -La identificación de la zona se realiza mediante la variable **tipo_cuenta**:  
120 -1. **Zona Urbana (tipo_cuenta = 0)**: snc_cuenta = Substring(ccatastral, 4) eliminando ceros a la izquierda y caracteres especiales.  
121 -2. **Zona Rural (tipo_cuenta = 1)**: snc_cuenta = padron::text (sin modificaciones).  
122 -  
123 -Integridad: Se debe aplicar ST_MakeValid(geom) en la inserción para prevenir errores de renderizado en GeoServer. 119 +Regla 26. El motor de importación debe aplicar una limpieza universal al campo snc_cuenta para asegurar el match tributario.
  120 +La identificación de la zona se realiza mediante la variable tipo_cuenta:
  121 +1. Zona Urbana (tipo_cuenta = 1): snc_cuenta = Substring(ccatastral, 4) eliminando ceros a la izquierda y caracteres especiales.
  122 +Algoritmo Java para Zona Urbana:
  123 +substring(3) (4ª posición).
  124 +replaceAll("[^a-zA-Z0-9]", "") (Eliminar guiones, puntos, etc.).
  125 +replaceFirst("^0+", "") (Eliminar ceros a la izquierda resultantes).
  126 +Algoritmo SQL para Zona Urbana:
  127 +SUBSTRING(ccatastral FROM 4) (Substring desde la 4ª posición).
  128 +REGEXP_REPLACE(..., '[^a-zA-Z0-9]', '', 'g') (Limpieza de caracteres especiales).
  129 +LTRIM(..., '0') (Eliminación de ceros a la izquierda).
  130 +
  131 +2. Zona Rural (tipo_cuenta = 0): snc_cuenta = padron::text (sin modificaciones).
  132 +Esta regla es mandatoria para toda municipalidad.
  133 +Se debe aplicar ST_MakeValid(geom) en la inserción para prevenir errores de renderizado en GeoServer.
124 Para la relación entre código SNC y código de municipio se debe utilizar la tabla ENTIDADES que se registra en public.snc_catalog_mapping 134 Para la relación entre código SNC y código de municipio se debe utilizar la tabla ENTIDADES que se registra en public.snc_catalog_mapping
125 135
126 Regla 27. Optimización y Cache GeoWebCache (GWC): 136 Regla 27. Optimización y Cache GeoWebCache (GWC):
@@ -134,3 +144,13 @@ La inserción en la base de datos se realizará mediante el uso directo de ST_Ge @@ -134,3 +144,13 @@ La inserción en la base de datos se realizará mediante el uso directo de ST_Ge
134 Implementar un TRUNCATE automático al inicio del proceso de importación. Si al importar los números son UTM, se deben transformar a 4326 antes de guardarlos. 144 Implementar un TRUNCATE automático al inicio del proceso de importación. Si al importar los números son UTM, se deben transformar a 4326 antes de guardarlos.
135 Las columnas de las tablas eXXX_lotes_activos deberá tener todas las columnas del SNC. 145 Las columnas de las tablas eXXX_lotes_activos deberá tener todas las columnas del SNC.
136 146
  147 +
  148 +Regla 29.
  149 +Para la construcción en la compilación, se usa JAVA21 del 192.168.1.123, en sincronía con la Regla 1.
  150 +
  151 +Regla 30. Arquitectura de Visualización en Dos Niveles.
  152 +El visor GIS utiliza una estrategia de superposición para optimizar el rendimiento y la claridad visual. Utiliza dos tipos de capas para mostrar los lotes: Una Capa Base (Lotes Grises) y múltiples Capas Temáticas (Lotes Coloreados).
  153 +1. Capa Base (Esqueleto): Utiliza la tabla física soberana `eXXX_lotes_activos`. Se publica en GeoServer con un estilo neutro (gris/transparente). Representa la totalidad del territorio y siempre es local y persistente.
  154 +2. Capa Temática (Inteligencia): Utiliza la vista lógica `vw_lotes_morosidad_XXX`. Es la capa dinámica que aplica el JOIN con el FDW municipal.
  155 +El Efecto Visual: La capa temática se superpone a la base. Aquellos lotes con coincidencia tributaria se "pintan" con el estilo de morosidad (semáforo), mientras que los lotes sin vínculo o sin deuda permanecen en gris (dejando ver la capa base), garantizando que la ciudad nunca se vea "incompleta".
  156 +
PERFIL-TECNOLOGICO.md 0 → 100644
  1 +# Perfil Tecnológico: Plataforma GIS-GEOSERVER (SNC + SIGEM)
  2 +
  3 +## 1. Visión Estratégica
  4 +La plataforma **GIS-GEOSERVER** representa la capa de inteligencia geográfica para la gestión municipal moderna. A diferencia de sistemas GIS aislados, esta arquitectura garantiza la **soberanía de los datos** al vincular el parcelario oficial de la Dirección Nacional de Catastro (SNC) con la realidad tributaria y administrativa de cada municipio en tiempo real.
  5 +
  6 +## 2. Núcleo Tecnológico (The Core)
  7 +
  8 +### Procesamiento Geoespacial Avanzado
  9 +* **Engine:** PostgreSQL 16 con extensión **PostGIS**.
  10 +* **Estándar de Geometría:** Procesamiento universal en **SRID 4326**. El sistema realiza transformaciones de coordenadas en tiempo real para asegurar que los datos del SNC (a menudo en UTM) se visualicen correctamente en cualquier visor web moderno.
  11 +* **Integridad Topológica:** Uso de funciones espaciales como `ST_MakeValid` y `ST_GeomFromGeoJSON` para garantizar que las capas se rendericen sin errores en el motor de mapas.
  12 +
  13 +### Orquestación de Mapas (Map Serving)
  14 +* **GeoServer:** Implementación de alto rendimiento sobre Java 21.
  15 +* **Protocolos Soportados:** WMS (Visualización), WFS (Intercambio de datos vectoriales) y REST API para administración automatizada de capas.
  16 +* **Optimización de Cache:** Uso intensivo de **GeoWebCache (GWC)** para reducir la carga del servidor y mejorar el tiempo de respuesta al usuario final mediante el pre-cacheo de teselas.
  17 +
  18 +## 3. Arquitectura de Integración (Interoperabilidad)
  19 +
  20 +### Virtualización de Datos mediante FDW
  21 +El sistema utiliza **Foreign Data Wrappers (FDW)** para "leer" las bases de datos municipales sin necesidad de duplicar la información.
  22 +* **Beneficio:** Si un contribuyente paga su impuesto en el sistema de gestión, el mapa se "pinta" automáticamente de un color diferente en el siguiente refresco de pantalla, sin intervención manual.
  23 +
  24 +### Normalización Dinámica (Regla 26)
  25 +Implementación de un motor de limpieza de cuentas catastrales que elimina la fricción entre los diferentes formatos de códigos de cuenta:
  26 +* **Limpieza Universal:** Algoritmos en Java y SQL que normalizan cuentas urbanas y rurales para asegurar que el `snc_cuenta` siempre encuentre su par tributario en el sistema de gestión.
  27 +
  28 +## 4. Gestión de Infraestructura y Resiliencia
  29 +
  30 +### Desarrollo y Despliegue (DevOps)
  31 +* **Stack:** Java 21 / Spring Boot 3.x / Maven.
  32 +* **CI/CD:** Automatización mediante **Jenkins**, permitiendo actualizaciones continuas con mínimo tiempo de inactividad.
  33 +* **Contenerización:** Despliegue basado en **Docker**, facilitando la escalabilidad y la portabilidad del entorno entre servidores.
  34 +
  35 +### Seguridad y Aislamiento Multi-Tenant
  36 +La arquitectura está diseñada para manejar múltiples municipios (Entidades) de forma aislada:
  37 +* **Aislamiento de Datos:** Cada municipio tiene su propio esquema y sus propias vistas de morosidad (`vw_lotes_morosidad_XXX`), garantizando la privacidad y seguridad de la información.
  38 +* **Soberanía Local:** El sistema diferencia entre la "Capa Base" (lotes físicos locales) y "Capas Temáticas" (vistas inteligentes), permitiendo que el municipio siempre tenga acceso a su inventario de tierras, incluso si los servicios externos están temporalmente fuera de línea.
  39 +
  40 +---
  41 +**Documento de Perfil Tecnológico v1.1**
  42 +*Preparado para revisión y expansión de hitos técnicos.*
PUNTO_DE_CONTROL_OVIEDO.md 0 → 100644
  1 +# Punto de Control - SIGEM-GIS (Coronel Oviedo 505)
  2 +**Fecha**: 16 de Abril de 2026
  3 +
  4 +## 1. Logros Alcanzados en la Sesión
  5 +* **Corrección de la Regla 26**: Se identificó y corrigió la lógica en el backend y en el documento de reglas. Oficialmente: `tipo_cuenta = 1` (Urbano) y `tipo_cuenta = 0` (Rural).
  6 +* **Saneamiento Nacional Masivo**: Se actualizó la tabla maestra soberana (`public.snc_raw_lotes_activos`) en el servidor **.123**. Se recalcularon los campos `snc_cuenta` para más de 3 millones de registros bajo la lógica correcta.
  7 +* **Impacto en Oviedo (505)**:
  8 + * De 26,882 lotes totales, pasamos a tener **18,804 cuentas catastrales correctamente pobladas** (14,719 Urbanos y 12,163 Rurales).
  9 + * Los lotes vinculados con éxito (en color) subieron a **10,474**.
  10 +* **Integridad Visual**: La capa base de lotes grises (`e505_lotes_activos`) se mantiene intacta mostrando toda la cartografía territorial, lista para la Landing Page.
  11 +
  12 +## 2. Estado Actual y Motivo de Pausa
  13 +La vinculación (`JOIN`) no alcanza al 100% de los lotes urbanos debido a una discrepancia de formatos detectada:
  14 +* **El SNC (Regla 26)** extrae la base de la cuenta. Ejemplo: **`101`**.
  15 +* **El FDW (Municipalidad)** tiene registrado el inmueble con guiones. Ejemplo: `21-0001-01`. Al limpiarlo con `REPLACE(..., '-', '')`, queda: **`21000101`**.
  16 +* **El Fallo**: Como `101` no es igual a `21000101`, el vínculo no se produce para miles de lotes, quedando en gris a pesar de existir la deuda.
  17 +
  18 +## 3. Próximos Pasos (Próxima Sesión)
  19 +* Queda pendiente investigar y definir cómo abordar el prefijo (ej. `"21"`) y los ceros de relleno que la municipalidad (FDW) usa, sin violar la restricción de no usar columnas fuera de las reglas establecidas.
  20 +* El usuario retomará el análisis de por qué falta este "pegamento" en la vinculación para proponer la solución conceptual definitiva.
apply_master_join_505.sql 0 → 100644
  1 +-- Reconstrucción de la Vista 505 según el SQL Maestro Definido
  2 +DROP VIEW IF EXISTS public.vw_lotes_morosidad_505 CASCADE;
  3 +
  4 +CREATE OR REPLACE VIEW public.vw_lotes_morosidad_505 AS
  5 +SELECT
  6 + lot.*,
  7 + liq.inm_ficha,
  8 + liq.inm_ctacatastral,
  9 + liq.trb_tributo,
  10 + liq.trb_total_deuda,
  11 + liq.trb_total_pago,
  12 + liq.ultimo_pago
  13 +FROM public.e505_lotes_activos lot
  14 +LEFT JOIN fdw_505.v_liq_entidad_totalxobjeto liq ON lot.snc_cuenta = REPLACE(liq.inm_ctacatastral, '-', '');
  15 +
  16 +-- Auditoría de Éxito de Coloreado (Con la nueva lógica de REPLACE)
  17 +SELECT count(*) as total_lotes, count(inm_ficha) as lotes_con_deuda_coloreados
  18 +FROM public.vw_lotes_morosidad_505;
check_505.sql 0 → 100644
  1 +SELECT * FROM public.snc_catalog_mapping WHERE entidad_id = '505';
check_bounds_505.sql 0 → 100644
  1 +SELECT entidad, boundno, boundse, latlong, zoom
  2 +FROM public.entidades
  3 +WHERE entidad = 505;
check_cols_505.sql 0 → 100644
  1 +SELECT column_name, data_type
  2 +FROM information_schema.columns
  3 +WHERE table_name = 'e505_lotes_activos'
  4 +AND table_schema = 'public';
check_count_505.sql 0 → 100644
  1 +SELECT tipo_cuenta, count(*) FROM public.e505_lotes_activos GROUP BY tipo_cuenta;
check_map_colors_505.sql 0 → 100644
  1 +SELECT
  2 + count(*) as total_geometrias,
  3 + count(inm_ficha) as lotes_con_deuda_coloreados,
  4 + round(count(inm_ficha)::numeric / count(*)::numeric * 100, 2) as cobertura_mapa
  5 +FROM public.vw_lotes_morosidad_505;
check_master_integrity.sql 0 → 100644
  1 +-- Verificación de atributos en la tabla maestra
  2 +SELECT
  3 + dpto,
  4 + dist,
  5 + count(*) as total,
  6 + count(NULLIF(snc_cuenta, '')) as con_cuenta_real,
  7 + count(NULLIF(ccatastral, '')) as con_catastro
  8 +FROM public.snc_raw_lotes_activos
  9 +WHERE dpto = 'F' AND dist = 1
  10 +GROUP BY dpto, dist;
check_migration_progress.sql 0 → 100644
  1 +SELECT count(*) as total_lotes_locales FROM public.snc_raw_lotes_activos;
check_oviedo_local_load.sql 0 → 100644
  1 +SELECT count(*) as total_lotes_oviedo FROM public.e505_lotes_activos;
check_oviedo_mapping.sql 0 → 100644
  1 +SELECT * FROM public.snc_catalog_mapping WHERE entidad_id = '505';
check_view_def.sql 0 → 100644
  1 +SELECT pg_get_viewdef('public.vw_lotes_morosidad_505', true);
check_vinculacion_505.sql 0 → 100644
  1 +-- Diagnóstico de Vinculación (Entidad 505)
  2 +SELECT
  3 + count(*) as total_registros,
  4 + count(inm_ficha) as vinculados,
  5 + round(count(inm_ficha)::numeric / count(*)::numeric * 100, 2) as porcentaje_exito
  6 +FROM public.vw_lotes_morosidad_505;
count_505.sql 0 → 100644
  1 +SELECT count(*) as total_lotes
  2 +FROM public.e505_lotes_activos;
create_master_snc_table.sql 0 → 100644
  1 +-- Creación de Tabla Maestra Nacional de Lotes SNC
  2 +DROP TABLE IF EXISTS public.snc_raw_lotes_activos CASCADE;
  3 +
  4 +CREATE TABLE public.snc_raw_lotes_activos (
  5 + sigem_id SERIAL PRIMARY KEY,
  6 + id integer,
  7 + objectid numeric,
  8 + id_parcela numeric,
  9 + dpto text,
  10 + dist smallint,
  11 + padron integer,
  12 + zona integer,
  13 + mz integer,
  14 + lote integer,
  15 + finca integer,
  16 + nro_matricula text,
  17 + ccatastral text,
  18 + obs text,
  19 + mz_agr text,
  20 + lote_agr text,
  21 + tipo_pavim text,
  22 + tipo_cuenta smallint,
  23 + hectareas numeric(20,6),
  24 + superficie_tierra numeric(20,6),
  25 + superficie_edificado numeric(20,6),
  26 + valor_tierra numeric(20,2),
  27 + valor_edificado numeric(20,2),
  28 + tipo smallint,
  29 + referencia smallint,
  30 + clave_comparacion text,
  31 + snc_cuenta text,
  32 + ccc text,
  33 + geom geometry(MultiPolygon, 4326),
  34 + fecha_ingesta timestamp default now()
  35 +);
  36 +
  37 +-- Índices de Performance
  38 +CREATE INDEX idx_snc_raw_geom ON public.snc_raw_lotes_activos USING GIST(geom);
  39 +CREATE INDEX idx_snc_raw_ccatastral ON public.snc_raw_lotes_activos(ccatastral);
  40 +CREATE INDEX idx_snc_raw_ubica ON public.snc_raw_lotes_activos(dpto, dist);
  41 +CREATE INDEX idx_snc_raw_ccc ON public.snc_raw_lotes_activos(ccc);
debug_join_precision.sql 0 → 100644
  1 +-- Comparativa técnica de claves para asegurar el JOIN
  2 +SELECT
  3 + 'LOCAL' as origen,
  4 + mz,
  5 + lote,
  6 + snc_cuenta,
  7 + ccatastral,
  8 + ('21-' || LPAD(mz::text, 4, '0') || '-' || LPAD(lote::text, 2, '0')) as clave_generada
  9 +FROM public.e505_lotes_activos
  10 +WHERE mz IS NOT NULL AND lote IS NOT NULL
  11 +LIMIT 5;
  12 +
  13 +SELECT
  14 + 'REMOTO (FDW)' as origen,
  15 + inm_ctacatastral,
  16 + REPLACE(inm_ctacatastral, '-', '') as limpia_fdw,
  17 + inm_ficha
  18 +FROM fdw_505.v_liq_entidad_totalxobjeto
  19 +LIMIT 5;
deduplicate_master_table.sql 0 → 100644
  1 +-- Limpieza de Duplicados en Tabla Maestra Nacional
  2 +DELETE FROM public.snc_raw_lotes_activos a
  3 +USING public.snc_raw_lotes_activos b
  4 +WHERE a.sigem_id > b.sigem_id
  5 + AND a.id_snc = b.id_snc
  6 + AND a.ccatastral = b.ccatastral;
  7 +
  8 +-- Verificación Final tras limpieza
  9 +SELECT count(*) as total_lotes_unicos FROM public.snc_raw_lotes_activos;
  10 +SELECT count(*) as total_oviedo_unico FROM public.snc_raw_lotes_activos WHERE dpto = 'F' AND dist = 1;
diagnostic_split.sql 0 → 100644
  1 +-- 1. MUESTRA URBANA (tipo_cuenta = 0)
  2 +-- Buscamos 5 lotes sin vínculo para ver por qué no cruzan
  3 +SELECT
  4 + 'URBANO (SNC)' as status,
  5 + id_snc, tipo_cuenta, snc_cuenta, ccatastral, mz, lote
  6 +FROM public.e505_lotes_activos
  7 +WHERE tipo_cuenta = 0 AND (snc_cuenta IS NOT NULL AND snc_cuenta != '')
  8 +LIMIT 5;
  9 +
  10 +-- 2. MUESTRA RURAL (tipo_cuenta = 1)
  11 +-- Buscamos 5 lotes sin vínculo
  12 +SELECT
  13 + 'RURAL (SNC)' as status,
  14 + id_snc, tipo_cuenta, snc_cuenta, padron, finca
  15 +FROM public.e505_lotes_activos
  16 +WHERE tipo_cuenta = 1 AND (snc_cuenta IS NOT NULL AND snc_cuenta != '')
  17 +LIMIT 5;
  18 +
  19 +-- 3. MUESTRA FDW (Para comparar formatos)
  20 +SELECT
  21 + 'FDW (Tributario)' as status,
  22 + inm_ctacatastral,
  23 + REPLACE(inm_ctacatastral, '-', '') as limpio,
  24 + inm_ficha
  25 +FROM fdw_505.v_liq_entidad_totalxobjeto
  26 +ORDER BY inm_ctacatastral
  27 +LIMIT 10;
fdw_inm_audit.sql 0 → 100644
  1 +-- 1. ESTADÍSTICA DE POBLACIÓN (Solo 'INM')
  2 +SELECT
  3 + count(*) as total_inm,
  4 + count(NULLIF(inm_ctacatastral, '')) as con_cuenta,
  5 + count(CASE WHEN inm_ctacatastral LIKE '%-%' THEN 1 END) as con_guiones,
  6 + count(CASE WHEN inm_ctacatastral NOT LIKE '%-%' AND inm_ctacatastral != '' THEN 1 END) as sin_guiones
  7 +FROM fdw_505.v_liq_entidad_totalxobjeto
  8 +WHERE trb_tributo = 'INM';
  9 +
  10 +-- 2. MUESTRA DE FORMATOS (Solo 'INM')
  11 +SELECT
  12 + 'INM - CON GUIONES' as categoria,
  13 + inm_ctacatastral,
  14 + REPLACE(inm_ctacatastral, '-', '') as limpia,
  15 + inm_ ficha,
  16 + trb_total_deuda
  17 +FROM fdw_505.v_liq_entidad_totalxobjeto
  18 +WHERE trb_tributo = 'INM' AND inm_ctacatastral LIKE '%-%'
  19 +LIMIT 5;
  20 +
  21 +SELECT
  22 + 'INM - SIN GUIONES' as categoria,
  23 + inm_ctacatastral,
  24 + inm_ficha,
  25 + trb_total_deuda
  26 +FROM fdw_505.v_liq_entidad_totalxobjeto
  27 +WHERE trb_tributo = 'INM' AND inm_ctacatastral NOT LIKE '%-%' AND inm_ctacatastral != ''
  28 +LIMIT 5;
fdw_inm_dash_sample.sql 0 → 100644
  1 +-- MUESTRA DE FORMATOS CON GUIONES (Solo 'INM')
  2 +SELECT
  3 + 'INM - CON GUIONES' as categoria,
  4 + inm_ctacatastral,
  5 + REPLACE(inm_ctacatastral, '-', '') as limpia,
  6 + inm_ficha,
  7 + trb_total_deuda
  8 +FROM fdw_505.v_liq_entidad_totalxobjeto
  9 +WHERE trb_tributo = 'INM' AND inm_ctacatastral LIKE '%-%'
  10 +LIMIT 10;
fdw_quality_audit.sql 0 → 100644
  1 +-- 1. ESTADÍSTICA DE POBLACIÓN DE CUENTAS EN FDW 505
  2 +SELECT
  3 + count(*) as total_registros_fdw,
  4 + count(NULLIF(inm_ctacatastral, '')) as con_cuenta_poblada,
  5 + (count(*) - count(NULLIF(inm_ctacatastral, ''))) as vacios_o_nulos
  6 +FROM fdw_505.v_liq_entidad_totalxobjeto;
  7 +
  8 +-- 2. MUESTRA DE FORMATOS EXISTENTES EN FDW
  9 +-- Registros con guiones
  10 +SELECT
  11 + 'CON GUIONES' as categoria,
  12 + inm_ctacatastral,
  13 + REPLACE(inm_ctacatastral, '-', '') as limpia,
  14 + inm_ficha
  15 +FROM fdw_505.v_liq_entidad_totalxobjeto
  16 +WHERE inm_ctacatastral LIKE '%-%'
  17 +LIMIT 5;
  18 +
  19 +-- Registros sin guiones (números puros)
  20 +SELECT
  21 + 'SIN GUIONES' as categoria,
  22 + inm_ctacatastral,
  23 + inm_ficha
  24 +FROM fdw_505.v_liq_entidad_totalxobjeto
  25 +WHERE inm_ctacatastral NOT LIKE '%-%' AND inm_ctacatastral != ''
  26 +LIMIT 5;
final_audit_505.sql 0 → 100644
  1 +-- Validación Final de Integridad (Regla 23 + Regla 26)
  2 +SELECT ccc, inm_ficha, trb_total_deuda
  3 +FROM public.vw_lotes_morosidad_505
  4 +WHERE trb_total_deuda > 0
  5 +LIMIT 10;
final_binary_audit.sql 0 → 100644
  1 +-- Auditoría de "Match" Real (Regla 23 vs Regla 26)
  2 +SELECT 'LOCAL (SNC)' as fuente, snc_cuenta, ccatastral
  3 +FROM public.e505_lotes_activos
  4 +LIMIT 5;
  5 +
  6 +SELECT 'REMOTO (MUNICIPIO)' as fuente, inm_ctacatastral, REPLACE(inm_ctacatastral, '-', '') as normalizada_remota
  7 +FROM fdw_505.v_liq_entidad_totalxobjeto
  8 +LIMIT 5;
final_integrity_check.sql 0 → 100644
  1 +SELECT count(*) as total, count(NULLIF(snc_cuenta, '')) as con_cuenta FROM public.e505_lotes_activos;
final_logic_verification.sql 0 → 100644
  1 +SELECT
  2 + 'e505_lotes_activos' as tabla,
  3 + count(*) as total,
  4 + count(NULLIF(snc_cuenta, '')) as con_cuenta,
  5 + count(CASE WHEN tipo_cuenta = 1 THEN 1 END) as urbanos,
  6 + count(CASE WHEN tipo_cuenta = 0 THEN 1 END) as rurales
  7 +FROM public.e505_lotes_activos;
  8 +
  9 +-- Comprobar si ahora el JOIN tiene más éxito
  10 +SELECT
  11 + count(*) as total_vinculados_con_color
  12 +FROM public.e505_lotes_activos lot
  13 +JOIN fdw_505.v_liq_entidad_totalxobjeto liq ON lot.snc_cuenta = REPLACE(liq.inm_ctacatastral, '-', '');
find_dist_21.sql 0 → 100644
  1 +-- Buscar el distrito 21 en todo el SNC
  2 +SELECT * FROM public.snc_catalog_mapping WHERE dist_snc = '21';
find_oviedo_real_codes.sql 0 → 100644
  1 +-- Búsqueda de códigos reales para Coronel Oviedo
  2 +SELECT * FROM public.snc_catalog_mapping
  3 +WHERE snc_nom_dist ILIKE '%OVIEDO%'
  4 + OR dist_snc = '21'
  5 + OR (dpto_snc = '5' AND dist_snc = '1');
fix_ccc_505.sql 0 → 100644
  1 +-- Saneamiento Masivo de Claves (Regla 26) para Coronel Oviedo (505)
  2 +UPDATE public.e505_lotes_activos
  3 +SET ccc = CASE
  4 + WHEN tipo_cuenta = 0 THEN LTRIM(REPLACE(SUBSTRING(ccatastral FROM 4), '-', ''), '0')
  5 + WHEN tipo_cuenta = 1 THEN padron
  6 + ELSE snc_cuenta
  7 +END;
  8 +
  9 +-- Verificación de Calidad (Comparativa de match)
  10 +SELECT 'AUDITORIA' as status, ccc, ccatastral, snc_cuenta
  11 +FROM public.e505_lotes_activos
  12 +LIMIT 5;
fix_master_table_columns.sql 0 → 100644
  1 +-- Ajuste de nombres de columnas para compatibilidad con el motor de ingesta masiva
  2 +ALTER TABLE public.snc_raw_lotes_activos RENAME COLUMN id TO id_snc;
  3 +ALTER TABLE public.snc_raw_lotes_activos RENAME COLUMN tipo TO tipo_parcela;
fix_regla26_perfected.sql 0 → 100644
  1 +-- Saneamiento Definitivo según Regla 26 (Perfected)
  2 +UPDATE public.e505_lotes_activos
  3 +SET snc_cuenta = LTRIM(REGEXP_REPLACE(SUBSTRING(ccatastral FROM 4), '[^a-zA-Z0-9]', '', 'g'), '0'),
  4 + ccc = LTRIM(REGEXP_REPLACE(SUBSTRING(ccatastral FROM 4), '[^a-zA-Z0-9]', '', 'g'), '0')
  5 +WHERE tipo_cuenta = 0;
  6 +
  7 +UPDATE public.e505_lotes_activos
  8 +SET snc_cuenta = padron,
  9 + ccc = padron
  10 +WHERE tipo_cuenta = 1;
  11 +
  12 +-- Auditoría de Coincidencia
  13 +SELECT ccatastral, snc_cuenta as cuenta_normalizada, ccc as join_key
  14 +FROM public.e505_lotes_activos
  15 +LIMIT 10;
full_record_audit.sql 0 → 100644
  1 +-- Extracción de Ficha Completa (Entidad 505)
  2 +(
  3 + SELECT 'URBANA' as origen_audit, *, ST_AsText(ST_Centroid(geom)) as centroide_text
  4 + FROM public.e505_lotes_activos WHERE tipo_cuenta = 0 LIMIT 5
  5 +)
  6 +UNION ALL
  7 +(
  8 + SELECT 'RURAL' as origen_audit, *, ST_AsText(ST_Centroid(geom)) as centroide_text
  9 + FROM public.e505_lotes_activos WHERE tipo_cuenta = 1 LIMIT 5
  10 +);
full_record_dump.sql 0 → 100644
  1 +-- Volcado vertical completo de los 10 registros de prueba
  2 +SELECT * FROM public.e505_lotes_activos ORDER BY tipo_cuenta ASC, id ASC;
get_bounds_master.sql 0 → 100644
  1 +SELECT * FROM dblink('host=192.168.1.254 user=postgres password=x25yvaga2017 dbname=sigemweb',
  2 + 'SELECT entidad, boundno, boundse, latlong, zoom FROM public.entidades WHERE entidad = 505')
  3 +AS t(entidad int, boundno text, boundse text, latlong text, zoom text);
national_sovereign_fix.sql 0 → 100644
  1 +-- RECALCULO MASIVO NACIONAL (REGLA 26 ACTUALIZADA)
  2 +-- 1 = URBANO (Catastro Substring 4)
  3 +-- 0 = RURAL (Padron Puro)
  4 +
  5 +UPDATE public.snc_raw_lotes_activos
  6 +SET
  7 + snc_cuenta = CASE
  8 + WHEN tipo_cuenta = 1 THEN LTRIM(REGEXP_REPLACE(SUBSTRING(ccatastral, 4), '[^a-zA-Z0-9]', '', 'g'), '0')
  9 + WHEN tipo_cuenta = 0 THEN padron::text
  10 + ELSE snc_cuenta
  11 + END,
  12 + ccc = CASE
  13 + WHEN tipo_cuenta = 1 THEN LTRIM(REGEXP_REPLACE(SUBSTRING(ccatastral, 4), '[^a-zA-Z0-9]', '', 'g'), '0')
  14 + WHEN tipo_cuenta = 0 THEN padron::text
  15 + ELSE ccc
  16 + END
  17 +WHERE tipo_cuenta IN (0, 1);
  18 +
  19 +-- Auditoria de Poblacion Nacional Post-Saneamiento
  20 +SELECT
  21 + tipo_cuenta,
  22 + count(*) as total,
  23 + count(NULLIF(snc_cuenta, '')) as con_cuenta
  24 +FROM public.snc_raw_lotes_activos
  25 +GROUP BY tipo_cuenta;
nature_of_lots_audit.sql 0 → 100644
  1 +-- AUDITORIA DE NATURALEZA DE LOTE (SNC)
  2 +SELECT
  3 + tipo_cuenta,
  4 + count(*) as total_registros,
  5 + count(NULLIF(padron::text, '')) as con_padron,
  6 + count(NULLIF(ccatastral, '')) as con_catastro,
  7 + count(CASE WHEN (padron IS NULL OR padron::text = '') AND (ccatastral IS NOT NULL AND ccatastral != '') THEN 1 END) as solo_catastro_prob_urbano,
  8 + count(CASE WHEN (padron IS NOT NULL AND padron::text != '') AND (ccatastral IS NULL OR ccatastral = '') THEN 1 END) as solo_padron_prob_rural
  9 +FROM public.snc_raw_lotes_activos
  10 +WHERE dpto = 'F' AND dist = 1
  11 +GROUP BY tipo_cuenta;
optimize_master_table.sql 0 → 100644
  1 +-- Optimización Post-Ingesta Nacional
  2 +CREATE INDEX IF NOT EXISTS idx_snc_raw_snc_cuenta ON public.snc_raw_lotes_activos(snc_cuenta);
  3 +VACUUM ANALYZE public.snc_raw_lotes_activos;
peek_fdw_keys.sql 0 → 100644
  1 +-- Muestra de claves catastrales en la base de datos de deudas (FDW)
  2 +SELECT inm_ctacatastral, inm_ficha
  3 +FROM fdw_505.v_liq_entidad_totalxobjeto
  4 +LIMIT 10;
peek_success_matches.sql 0 → 100644
  1 +-- Casos donde el JOIN del usuario SI funciona
  2 +SELECT
  3 + lot.snc_cuenta,
  4 + liq.inm_ctacatastral,
  5 + REPLACE(liq.inm_ctacatastral, '-', '') as match_db,
  6 + lot.mz,
  7 + lot.lote
  8 +FROM public.e505_lotes_activos lot
  9 +JOIN fdw_505.v_liq_entidad_totalxobjeto liq ON lot.snc_cuenta = REPLACE(liq.inm_ctacatastral, '-', '')
  10 +LIMIT 5;
query_raw_districts.sql 0 → 100644
  1 +-- Consulta del Registro Crudo en el Departamento F
  2 +SELECT * FROM public.snc_raw_distritos WHERE cod_dpto = 'E' AND codigo = 'E01';
  3 +SELECT * FROM public.snc_raw_distritos WHERE cod_dpto = 'F' AND codigo = 'F01';
recalculate_snc_cuenta.sql 0 → 100644
  1 +-- Simulación de Recálculo snc_cuenta (Regla 26) para Zona Urbana y Rural
  2 +(
  3 + SELECT
  4 + 'URBANA (0)' as zona,
  5 + ccatastral as entrada_cruda,
  6 + LTRIM(REGEXP_REPLACE(SUBSTRING(ccatastral FROM 4), '[^a-zA-Z0-9]', '', 'g'), '0') as snc_cuenta_recalculada
  7 + FROM public.e505_lotes_activos
  8 + WHERE tipo_cuenta = 0
  9 + LIMIT 5
  10 +)
  11 +UNION ALL
  12 +(
  13 + SELECT
  14 + 'RURAL (1)' as zona,
  15 + padron as entrada_cruda,
  16 + padron::text as snc_cuenta_recalculada
  17 + FROM public.e505_lotes_activos
  18 + WHERE tipo_cuenta = 1
  19 + LIMIT 5
  20 +);
show_discrepancies.sql 0 → 100644
  1 +-- 10 Registros de la Cartografía Local (SNC)
  2 +SELECT 'LOCAL_SNC' as fuente, ccatastral, snc_cuenta, ccc
  3 +FROM public.e505_lotes_activos
  4 +LIMIT 10;
  5 +
  6 +-- 10 Registros del Sistema Municipal (SIGEM)
  7 +SELECT 'REMOTO_SIGEM' as fuente, inm_ctacatastral, REPLACE(inm_ctacatastral, '-', '') as join_esperado, trb_total_deuda
  8 +FROM fdw_505.v_liq_entidad_totalxobjeto
  9 +WHERE trb_total_deuda > 0
  10 +LIMIT 10;
show_mapping_505.sql 0 → 100644
  1 +SELECT * FROM public.snc_catalog_mapping WHERE entidad_id = '505';
src/main/java/com/sigem/gis/WebConfig.java
@@ -14,6 +14,8 @@ public class WebConfig implements WebMvcConfigurer { @@ -14,6 +14,8 @@ public class WebConfig implements WebMvcConfigurer {
14 // Enrutador amigable (Friendly URLs sin ".html") para el Frontend 14 // Enrutador amigable (Friendly URLs sin ".html") para el Frontend
15 registry.addViewController("/login").setViewName("forward:/login.html"); 15 registry.addViewController("/login").setViewName("forward:/login.html");
16 registry.addViewController("/mapas").setViewName("forward:/mapas.html"); 16 registry.addViewController("/mapas").setViewName("forward:/mapas.html");
  17 + registry.addViewController("/landing").setViewName("forward:/landing.html");
  18 + registry.addViewController("/widgets").setViewName("forward:/widgets.html");
17 registry.addViewController("/").setViewName("forward:/login.html"); 19 registry.addViewController("/").setViewName("forward:/login.html");
18 } 20 }
19 21
src/main/java/com/sigem/gis/controller/SncImportController.java
@@ -3,6 +3,7 @@ package com.sigem.gis.controller; @@ -3,6 +3,7 @@ package com.sigem.gis.controller;
3 import org.springframework.beans.factory.annotation.Autowired; 3 import org.springframework.beans.factory.annotation.Autowired;
4 import org.springframework.beans.factory.annotation.Qualifier; 4 import org.springframework.beans.factory.annotation.Qualifier;
5 import org.springframework.jdbc.core.JdbcTemplate; 5 import org.springframework.jdbc.core.JdbcTemplate;
  6 +import org.springframework.http.ResponseEntity;
6 import org.springframework.web.bind.annotation.*; 7 import org.springframework.web.bind.annotation.*;
7 import org.springframework.web.client.RestTemplate; 8 import org.springframework.web.client.RestTemplate;
8 import java.util.*; 9 import java.util.*;
@@ -511,7 +512,7 @@ public class SncImportController { @@ -511,7 +512,7 @@ public class SncImportController {
511 System.out.println(String.format("[%d/%d] PROCESANDO ENTIDAD %s (SNC: %s-%s)", current, total, eid, 512 System.out.println(String.format("[%d/%d] PROCESANDO ENTIDAD %s (SNC: %s-%s)", current, total, eid,
512 dpto, dist)); 513 dpto, dist));
513 try { 514 try {
514 - importDistrict(eid, dpto, dist, false); 515 + importDistrict(eid, dpto, dist, 0, false, false);
515 } catch (Exception e) { 516 } catch (Exception e) {
516 System.err.println("!!! FALLO CRÍTICO EN ENTIDAD " + eid + ": " + e.getMessage()); 517 System.err.println("!!! FALLO CRÍTICO EN ENTIDAD " + eid + ": " + e.getMessage());
517 } 518 }
@@ -526,20 +527,36 @@ public class SncImportController { @@ -526,20 +527,36 @@ public class SncImportController {
526 } 527 }
527 528
528 @GetMapping("/snc/{entityId}/{dpto}/{dist}") 529 @GetMapping("/snc/{entityId}/{dpto}/{dist}")
529 - public String importDistrict( 530 + public ResponseEntity<String> importDistrict(
530 @PathVariable String entityId, 531 @PathVariable String entityId,
531 @PathVariable String dpto, 532 @PathVariable String dpto,
532 @PathVariable String dist, 533 @PathVariable String dist,
533 - @RequestParam(defaultValue = "true") boolean processFdw) { 534 + @RequestParam(defaultValue = "0") int limit,
  535 + @RequestParam(defaultValue = "true") boolean processFdw,
  536 + @RequestParam(defaultValue = "false") boolean forceWfs) {
  537 +
  538 + String tableName = "public.e" + entityId + "_lotes_activos";
  539 + createSncTableIfNotExists(tableName);
  540 +
  541 + int batchSize = 0;
  542 + if (forceWfs) {
  543 + // RECUPERACIÓN: Importación desde WFS directamente a la Maestra y luego al municipio
  544 + batchSize = importFromWfs(tableName, dpto, dist, limit);
  545 + } else {
  546 + // OPERACIÓN NORMAL: Transferencia local soberana
  547 + batchSize = importDistrictInternal(tableName, dpto, dist, limit);
  548 + }
  549 +
  550 + if (batchSize > 0 && processFdw) {
  551 + processFdwAndViews(entityId);
  552 + }
  553 +
  554 + return ResponseEntity.ok("OK: Importación " + entityId + " finalizada. Registros: " + batchSize);
  555 + }
534 556
  557 + private int importFromWfs(String tableName, String dpto, String dist, int limit) {
535 try { 558 try {
536 - String tableName = "public.e" + entityId + "_lotes_activos";  
537 -  
538 - // 1. Garantizar existencia de la tabla e Iniciar Limpieza  
539 - createSncTableIfNotExists(tableName);  
540 - gisJdbcTemplate.execute("TRUNCATE TABLE " + tableName + " CASCADE");  
541 -  
542 - // 2. Construcción Robusta de URL (Regla 28) 559 + System.out.println(">>> RECUPERACIÓN WFS: " + dpto + "-" + dist + " -> Activos y Maestra");
543 String url = org.springframework.web.util.UriComponentsBuilder 560 String url = org.springframework.web.util.UriComponentsBuilder
544 .fromHttpUrl("https://www.catastro.gov.py/geoserver/ows") 561 .fromHttpUrl("https://www.catastro.gov.py/geoserver/ows")
545 .queryParam("service", "WFS") 562 .queryParam("service", "WFS")
@@ -553,90 +570,137 @@ public class SncImportController { @@ -553,90 +570,137 @@ public class SncImportController {
553 .toUriString(); 570 .toUriString();
554 571
555 Map<String, Object> response = restTemplate.getForObject(url, Map.class); 572 Map<String, Object> response = restTemplate.getForObject(url, Map.class);
556 - if (response == null) {  
557 - System.err.println("!!! Respuesta NULL para Entidad " + entityId);  
558 - return "ERR: Respuesta NULL del SNC";  
559 - } 573 + if (response == null) return 0;
560 574
561 List<Map<String, Object>> features = (List<Map<String, Object>>) response.get("features"); 575 List<Map<String, Object>> features = (List<Map<String, Object>>) response.get("features");
562 - if (features == null || features.isEmpty()) {  
563 - System.out.println("--- CERO CARGAS para " + entityId + " (Filtro: " + dpto + "-" + dist + ")");  
564 - return "WARN: No se encontraron registros";  
565 - } 576 + if (features == null || features.isEmpty()) return 0;
566 577
567 com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); 578 com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
568 List<Object[]> batchArgs = new ArrayList<>(); 579 List<Object[]> batchArgs = new ArrayList<>();
569 580
570 for (Map<String, Object> feature : features) { 581 for (Map<String, Object> feature : features) {
571 Map<String, Object> props = (Map<String, Object>) feature.get("properties"); 582 Map<String, Object> props = (Map<String, Object>) feature.get("properties");
572 -  
573 - // Intento resiliente de captura de geometría (Regla 28)  
574 - Object shapeObj = props.get("shape");  
575 - if (shapeObj == null) {  
576 - shapeObj = feature.get("geometry");  
577 - }  
578 -  
579 - String ccatastral = (String) props.get("ccatastral");  
580 Integer tc = (Integer) props.get("tipo_cuenta"); 583 Integer tc = (Integer) props.get("tipo_cuenta");
  584 + Object shapeObj = props.get("shape") != null ? props.get("shape") : feature.get("geometry");
  585 + String ccatastral = (String) props.get("ccatastral");
581 Object padronObj = props.get("padron"); 586 Object padronObj = props.get("padron");
582 String padronStr = padronObj != null ? String.valueOf(padronObj) : ""; 587 String padronStr = padronObj != null ? String.valueOf(padronObj) : "";
583 588
584 - // REGLA 26: Normalización Universal de Cartografía SNC (Actualizada)  
585 String snc_cuenta = ""; 589 String snc_cuenta = "";
586 - if (tc != null && tc == 0) {  
587 - // 1. Zona Urbana (tipo_cuenta = 0): Substring(ccatastral, 4) eliminando ceros 590 + if (tc != null && tc == 1) {
  591 + // URBANO (tipo_cuenta = 1): Substring(ccatastral, 4)
588 if (ccatastral != null && ccatastral.length() >= 4) { 592 if (ccatastral != null && ccatastral.length() >= 4) {
589 - snc_cuenta = ccatastral.substring(3).replaceAll("^0+", "").replaceAll("[^a-zA-Z0-9]", "");  
590 - } else if (!padronStr.isEmpty()) {  
591 - snc_cuenta = padronStr.replaceAll("^0+", ""); 593 + String cleaned = ccatastral.substring(3).replaceAll("[^a-zA-Z0-9]", "");
  594 + snc_cuenta = cleaned.replaceFirst("^0+", "");
592 } 595 }
593 - } else if (tc != null && tc == 1) {  
594 - // 2. Zona Rural (tipo_cuenta = 1): padron::text (sin modificaciones) 596 + } else if (tc != null && tc == 0) {
  597 + // RURAL (tipo_cuenta = 0): Padron
595 snc_cuenta = padronStr; 598 snc_cuenta = padronStr;
596 } 599 }
597 600
598 try { 601 try {
599 - String geomJson = mapper.writeValueAsString(shapeObj);  
600 - if (shapeObj == null)  
601 - continue;  
602 -  
603 batchArgs.add(new Object[] { 602 batchArgs.add(new Object[] {
604 - props.get("id"), props.get("objectid"), props.get("id_parcela"),  
605 - props.get("dpto"), props.get("dist"), props.get("padron"),  
606 - props.get("zona"), props.get("mz"), props.get("lote"),  
607 - props.get("finca"), props.get("nro_matricula"),  
608 - ccatastral, props.get("obs"),  
609 - props.get("mz_agr"), props.get("lote_agr"),  
610 - props.get("tipo_pavim"), tc,  
611 - props.get("hectareas"), props.get("superficie_tierra"),  
612 - props.get("superficie_edificado"), props.get("valor_tierra"),  
613 - props.get("valor_edificado"), props.get("tipo"),  
614 - props.get("referencia"), props.get("clave_comparacion"),  
615 - geomJson, snc_cuenta, ccatastral 603 + props.get("id"), props.get("objectid"), props.get("id_parcela"),
  604 + props.get("dpto"), props.get("dist"), props.get("padron"),
  605 + props.get("zona"), props.get("mz"), props.get("lote"),
  606 + props.get("finca"), props.get("nro_matricula"),
  607 + ccatastral, props.get("obs"),
  608 + props.get("mz_agr"), props.get("lote_agr"),
  609 + props.get("tipo_pavim"), tc,
  610 + props.get("hectareas"), props.get("superficie_tierra"),
  611 + props.get("superficie_edificado"), props.get("valor_tierra"),
  612 + props.get("valor_edificado"), props.get("tipo"),
  613 + props.get("referencia"), props.get("clave_comparacion"),
  614 + mapper.writeValueAsString(shapeObj), snc_cuenta, snc_cuenta
616 }); 615 });
617 - } catch (Exception e) {  
618 - } 616 + } catch (Exception e) {}
619 } 617 }
620 618
621 - String insertSql = "INSERT INTO " + tableName + " (" +  
622 - "id_snc, objectid, id_parcela, dpto, dist, padron, zona, mz, lote, " +  
623 - "finca, nro_matricula, ccatastral, obs, mz_agr, lote_agr, tipo_pavim, " +  
624 - "tipo_cuenta, hectareas, superficie_tierra, superficie_edificado, " +  
625 - "valor_tierra, valor_edificado, tipo_parcela, referencia, clave_comparacion, " +  
626 - "geom, snc_cuenta, ccc) " +  
627 - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ST_Multi(ST_CollectionExtract(ST_MakeValid(ST_SetSRID(ST_GeomFromGeoJSON(?), 4326)), 3)), ?, ?)"; 619 + String columns = "id_snc, objectid, id_parcela, dpto, dist, padron, zona, mz, lote, finca, nro_matricula, ccatastral, obs, mz_agr, lote_agr, tipo_pavim, tipo_cuenta, hectareas, superficie_tierra, superficie_edificado, valor_tierra, valor_edificado, tipo_parcela, referencia, clave_comparacion, geom, snc_cuenta, ccc";
  620 + String values = "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ST_Multi(ST_CollectionExtract(ST_MakeValid(ST_SetSRID(ST_GeomFromGeoJSON(?), 4326)), 3)), ?, ?";
  621 +
  622 + gisJdbcTemplate.execute("DELETE FROM public.snc_raw_lotes_activos WHERE dpto = '" + dpto + "' AND dist = " + dist);
  623 + gisJdbcTemplate.batchUpdate("INSERT INTO public.snc_raw_lotes_activos (" + columns + ") VALUES (" + values + ")", batchArgs);
  624 +
  625 + gisJdbcTemplate.execute("TRUNCATE TABLE " + tableName + " CASCADE");
  626 + gisJdbcTemplate.batchUpdate("INSERT INTO " + tableName + " (" + columns + ") VALUES (" + values + ")", batchArgs);
  627 +
  628 + return batchArgs.size();
  629 + } catch (Exception e) { e.printStackTrace(); return 0; }
  630 + }
  631 +
  632 + @GetMapping("/snc/import-nacional")
  633 + public String importNacional() {
  634 + new Thread(() -> {
  635 + try {
  636 + System.out.println(">>> INICIANDO INGESTA NACIONAL SNC...");
  637 + String sql = "SELECT cod_dpto, cod_dist, nom_dist FROM public.snc_raw_distritos ORDER BY cod_dpto, cod_dist";
  638 + List<Map<String, Object>> distritos = gisJdbcTemplate.queryForList(sql);
  639 +
  640 + int total = distritos.size();
  641 + int current = 0;
  642 + long startAll = System.currentTimeMillis();
628 643
629 - gisJdbcTemplate.batchUpdate(insertSql, batchArgs);  
630 - System.out.println("+++ EXITO: " + entityId + " -> Inyectados " + batchArgs.size() + " registros."); 644 + for (Map<String, Object> d : distritos) {
  645 + current++;
  646 + String dpto = String.valueOf(d.get("cod_dpto"));
  647 + String dist = String.valueOf(d.get("cod_dist"));
  648 + String nombre = (String) d.get("nom_dist");
631 649
632 - if (processFdw) {  
633 - processFdwAndViews(entityId); 650 + System.out.println(String.format("### [%d/%d] PROCESANDO DISTRITO: %s (%s-%s)",
  651 + current, total, nombre, dpto, dist));
  652 +
  653 + try {
  654 + importDistrictInternal("public.snc_raw_lotes_activos", dpto, dist, 0);
  655 + } catch (Exception e) {
  656 + System.err.println("!!! Error en distrito " + nombre + ": " + e.getMessage());
  657 + }
  658 + }
  659 +
  660 + long duration = (System.currentTimeMillis() - startAll) / 1000 / 60;
  661 + System.out.println(">>> INGESTA NACIONAL COMPLETADA EN " + duration + " MINUTOS.");
  662 + } catch (Exception e) {
  663 + e.printStackTrace();
634 } 664 }
  665 + }).start();
  666 + return "OK: Proceso de Ingesta Nacional iniciado en segundo plano. Verifique logs.";
  667 + }
635 668
636 - return "OK: " + entityId + " (" + features.size() + " recs)"; 669 + private int importDistrictInternal(String tableName, String dpto, String dist, int limit) {
  670 + try {
  671 + System.out.println(">>> IMPORTACIÓN LOCAL: " + dpto + "-" + dist + " -> " + tableName);
  672 +
  673 + // 1. Limpiar tabla destino
  674 + gisJdbcTemplate.execute("TRUNCATE TABLE " + tableName + " CASCADE");
  675 +
  676 + // 2. Transferencia Interna de Alta Velocidad (Regla 26 ya aplicada en Master)
  677 + String columns = "id_snc, objectid, id_parcela, dpto, dist, padron, zona, mz, lote, finca, nro_matricula, ccatastral, obs, mz_agr, lote_agr, tipo_pavim, tipo_cuenta, hectareas, superficie_tierra, superficie_edificado, valor_tierra, valor_edificado, tipo_parcela, referencia, clave_comparacion, geom, snc_cuenta, ccc";
  678 +
  679 + String sql = "INSERT INTO " + tableName + " (" + columns + ") " +
  680 + "SELECT id_snc, objectid, id_parcela, dpto, dist, padron, zona, mz, lote, finca, nro_matricula, ccatastral, obs, mz_agr, lote_agr, tipo_pavim, tipo_cuenta, hectareas, superficie_tierra, superficie_edificado, valor_tierra, valor_edificado, tipo_parcela, referencia, clave_comparacion, geom, snc_cuenta, ccc " +
  681 + "FROM public.snc_raw_lotes_activos " +
  682 + "WHERE dpto = ? AND dist = ?::smallint";
  683 +
  684 + int rows;
  685 + if (limit > 0) {
  686 + // Modo Calidad Balanceado (5 Urbanos / 5 Rurales)
  687 + String limitSql = "INSERT INTO " + tableName + " (" + columns + ") " +
  688 + "(SELECT " + columns + " FROM public.snc_raw_lotes_activos WHERE dpto = ? AND dist = ?::smallint AND tipo_cuenta = 0 LIMIT ?) " +
  689 + "UNION ALL " +
  690 + "(SELECT " + columns + " FROM public.snc_raw_lotes_activos WHERE dpto = ? AND dist = ?::smallint AND tipo_cuenta = 1 LIMIT ?)";
  691 +
  692 + int half = limit / 2;
  693 + rows = gisJdbcTemplate.update(limitSql, dpto, dist, half, dpto, dist, half);
  694 + } else {
  695 + rows = gisJdbcTemplate.update(sql, dpto, dist);
  696 + }
  697 +
  698 + System.out.println("+++ EXITO LOCAL: Ubicados " + rows + " registros para " + tableName);
  699 + return rows;
637 } catch (Exception e) { 700 } catch (Exception e) {
638 - System.err.println("!!! FALLO en Importación de " + entityId + ": " + e.getMessage());  
639 - return "ERR: " + entityId + " -> " + e.getMessage(); 701 + System.err.println("!!! FALLO en Transferencia Local: " + e.getMessage());
  702 + e.printStackTrace();
  703 + return 0;
640 } 704 }
641 } 705 }
642 706
@@ -730,7 +794,7 @@ public class SncImportController { @@ -730,7 +794,7 @@ public class SncImportController {
730 entityId, fdwSchema)); 794 entityId, fdwSchema));
731 795
732 gisJdbcTemplate.execute("CREATE OR REPLACE VIEW public.vw_lotes_morosidad_" + entityId + " AS " + 796 gisJdbcTemplate.execute("CREATE OR REPLACE VIEW public.vw_lotes_morosidad_" + entityId + " AS " +
733 - "SELECT lot.*, liq.inm_ficha, liq.inm_ctacatastral, liq.trb_total_deuda, liq.trb_total_pago, liq.ultimo_pago " 797 + "SELECT lot.*, liq.inm_ficha, liq.inm_ctacatastral, liq.trb_tributo, liq.trb_total_deuda, liq.trb_total_pago, liq.ultimo_pago "
734 + 798 +
735 "FROM public.e" + entityId + "_lotes_activos lot " + 799 "FROM public.e" + entityId + "_lotes_activos lot " +
736 "LEFT JOIN " + fdwSchema 800 "LEFT JOIN " + fdwSchema
src/main/java/com/sigem/gis/security/SecurityConfig.java
@@ -30,6 +30,7 @@ public class SecurityConfig { @@ -30,6 +30,7 @@ public class SecurityConfig {
30 .requestMatchers("/api/admin/**").permitAll() // Admin FDW 30 .requestMatchers("/api/admin/**").permitAll() // Admin FDW
31 .requestMatchers("/api/gis/**").permitAll() // API Datos GIS (Estadísticas) 31 .requestMatchers("/api/gis/**").permitAll() // API Datos GIS (Estadísticas)
32 .requestMatchers("/api/import/**").permitAll() // Importador SNC 32 .requestMatchers("/api/import/**").permitAll() // Importador SNC
  33 + .requestMatchers("/api/fdw/**").permitAll() // Orquestación FDW
33 .requestMatchers("/login.html", "/", "/mapas/**", "/mapas.html", "/login", "/error", "/landing", "/landing.html", "/widgets", "/widgets.html").permitAll() 34 .requestMatchers("/login.html", "/", "/mapas/**", "/mapas.html", "/login", "/error", "/landing", "/landing.html", "/widgets", "/widgets.html").permitAll()
34 .requestMatchers("/mapas_institucional.html").permitAll() 35 .requestMatchers("/mapas_institucional.html").permitAll()
35 .requestMatchers("/css/**", "/js/**", "/img/**", "/vendor/**").permitAll() // Recursos 36 .requestMatchers("/css/**", "/js/**", "/img/**", "/vendor/**").permitAll() // Recursos
src/main/java/com/sigem/gis/service/FdwService.java
@@ -51,7 +51,8 @@ public class FdwService { @@ -51,7 +51,8 @@ public class FdwService {
51 String serverName = "srv_mun_" + entidadId; 51 String serverName = "srv_mun_" + entidadId;
52 String schemaName = "fdw_" + entidadId; 52 String schemaName = "fdw_" + entidadId;
53 53
54 - // 2. Ejecutar comandos DDL en el servidor PostGIS (.123) - RECREACIÓN OBLIGATORIA 54 + // 2. Ejecutar comandos DDL en el servidor PostGIS (.123) - RECREACIÓN
  55 + // OBLIGATORIA
55 try { 56 try {
56 // Recreación del Servidor y Mapeo 57 // Recreación del Servidor y Mapeo
57 gisJdbcTemplate.execute("DROP SERVER IF EXISTS " + serverName + " CASCADE"); 58 gisJdbcTemplate.execute("DROP SERVER IF EXISTS " + serverName + " CASCADE");
@@ -74,20 +75,24 @@ public class FdwService { @@ -74,20 +75,24 @@ public class FdwService {
74 75
75 // Vista de Auditoría (MVT) - REGLA 23 76 // Vista de Auditoría (MVT) - REGLA 23
76 String viewLotesName = "vw_lotes_morosidad_" + entidadId; 77 String viewLotesName = "vw_lotes_morosidad_" + entidadId;
  78 + gisJdbcTemplate.execute("DROP VIEW IF EXISTS public." + viewLotesName + " CASCADE");
77 gisJdbcTemplate.execute(String.format( 79 gisJdbcTemplate.execute(String.format(
78 "CREATE OR REPLACE VIEW public.%s AS " + 80 "CREATE OR REPLACE VIEW public.%s AS " +
79 - "SELECT l.*, m.inm_ficha, m.inm_ctacatastral, m.trb_total_deuda, m.trb_total_pago, m.ultimo_pago " +  
80 - "FROM %s l " +  
81 - "LEFT JOIN %s.v_liq_entidad_totalxobjeto m ON l.snc_cuenta = REPLACE(m.inm_ctacatastral, '-', '')", 81 + "SELECT lot.*, liq.inm_ficha, liq.inm_ctacatastral, liq.trb_tributo, liq.trb_total_deuda, liq.trb_total_pago, liq.ultimo_pago "
  82 + +
  83 + "FROM %s lot " +
  84 + "LEFT JOIN %s.v_liq_entidad_totalxobjeto liq ON lot.snc_cuenta = REPLACE(liq.inm_ctacatastral, '-', '')",
82 viewLotesName, tableLotes, schemaName)); 85 viewLotesName, tableLotes, schemaName));
83 86
84 // Vista PNG FULL (WMS) - REGLA 23 87 // Vista PNG FULL (WMS) - REGLA 23
85 String viewWmsName = "vw_lotes_wms_" + entidadId; 88 String viewWmsName = "vw_lotes_wms_" + entidadId;
  89 + gisJdbcTemplate.execute("DROP VIEW IF EXISTS public." + viewWmsName + " CASCADE");
86 gisJdbcTemplate.execute(String.format( 90 gisJdbcTemplate.execute(String.format(
87 "CREATE OR REPLACE VIEW public.%s AS " + 91 "CREATE OR REPLACE VIEW public.%s AS " +
88 - "SELECT l.*, m.inm_ficha, m.inm_ctacatastral, m.trb_total_deuda, m.trb_total_pago, m.ultimo_pago " +  
89 - "FROM %s l " +  
90 - "LEFT JOIN %s.v_liq_entidad_totalxobjeto m ON l.snc_cuenta = REPLACE(m.inm_ctacatastral, '-', '')", 92 + "SELECT lot.*, liq.inm_ficha, liq.inm_ctacatastral, liq.trb_tributo, liq.trb_total_deuda, liq.trb_total_pago, liq.ultimo_pago "
  93 + +
  94 + "FROM %s lot " +
  95 + "LEFT JOIN %s.v_liq_entidad_totalxobjeto liq ON lot.snc_cuenta = REPLACE(liq.inm_ctacatastral, '-', '')",
91 viewWmsName, tableLotes, schemaName)); 96 viewWmsName, tableLotes, schemaName));
92 97
93 // 4. Sincronización con GeoServer 98 // 4. Sincronización con GeoServer
src/main/resources/static/login.html
1 <!DOCTYPE html> 1 <!DOCTYPE html>
2 <html lang="es"> 2 <html lang="es">
3 <head> 3 <head>
4 - <meta charset="UTF-8">  
5 - <meta name="viewport" content="width=device-width, initial-scale=1.0">  
6 - <title>SIGEM GIS - Login</title>  
7 - <style>  
8 - body, html { height: 100%; margin: 0; font-family: 'Inter', sans-serif; background: #0f172a; color: #fff; display: flex; align-items: center; justify-content: center; overflow: hidden; }  
9 - .login-card { background: #1e293b; padding: 40px; border-radius: 20px; box-shadow: 0 20px 50px rgba(0,0,0,0.5); width: 100%; max-width: 400px; border: 1px solid rgba(255,255,255,0.1); }  
10 - .logo { text-align: center; margin-bottom: 30px; font-weight: 800; font-size: 24px; color: #3b82f6; letter-spacing: -1px; }  
11 - .form-group { margin-bottom: 20px; }  
12 - label { display: block; font-size: 11px; text-transform: uppercase; color: #64748b; font-weight: 700; margin-bottom: 8px; letter-spacing: 0.5px; }  
13 - input, select { width: 100%; padding: 12px 16px; background: #0f172a; border: 1px solid #334155; border-radius: 10px; color: #fff; font-size: 14px; transition: all 0.2s; }  
14 - input:focus, select:focus { border-color: #3b82f6; outline: none; box-shadow: 0 0 0 3px rgba(59,130,246,0.2); }  
15 - .login-btn { width: 100%; padding: 14px; background: #3b82f6; color: #fff; border: none; border-radius: 10px; font-weight: 600; cursor: pointer; transition: all 0.2s; margin-top: 20px; }  
16 - .login-btn:hover { background: #2563eb; transform: translateY(-1px); }  
17 - #error-msg { color: #ef4444; font-size: 13px; margin-top: 15px; text-align: center; display: none; }  
18 - </style> 4 + <meta charset="utf-8">
  5 + <meta name="viewport" content="width=device-width, initial-scale=1">
  6 + <title>Inicie Sesión - Ecosistema SIGEM</title>
  7 +
  8 + <!-- Google Font: Source Sans Pro -->
  9 + <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700&display=fallback">
  10 + <!-- Font Awesome -->
  11 + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
  12 + <!-- Theme style AdminLTE 3 -->
  13 + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/admin-lte@3.2/dist/css/adminlte.min.css">
  14 + <style>
  15 + .login-page {
  16 + background: linear-gradient(135deg, #e0eafc 0%, #cfdef3 100%);
  17 + }
  18 + .card-primary.card-outline {
  19 + border-top: 3px solid #0056b3;
  20 + }
  21 + .btn-primary {
  22 + background-color: #0056b3;
  23 + border-color: #0056b3;
  24 + }
  25 + .btn-primary:hover {
  26 + background-color: #004494;
  27 + border-color: #004494;
  28 + }
  29 + .login-logo a {
  30 + color: #333 !important;
  31 + }
  32 + #municipioSearch::placeholder {
  33 + font-size: 0.85rem;
  34 + }
  35 + </style>
19 </head> 36 </head>
20 -<body>  
21 - <div class="login-card">  
22 - <div class="logo">SIGEM<span style="color:#fff">WEB</span> GIS</div>  
23 - <form id="loginForm">  
24 - <div class="form-group">  
25 - <label>Municipalidad (Entidad)</label>  
26 - <select id="entidad" required></select> 37 +<body class="hold-transition login-page">
  38 +<div class="login-box" style="width: 400px;">
  39 + <div class="login-logo">
  40 + <a href="#"><b>SIGEM</b>WEB</a>
  41 + </div>
  42 + <div class="card card-outline card-primary shadow-lg">
  43 + <div class="card-body login-card-body rounded">
  44 + <p class="login-box-msg font-weight-bold" style="color: #555;">Visor Georreferenciado Multi-Tenant</p>
  45 +
  46 + <form id="loginForm">
  47 + <label for="municipioSearch" class="text-xs text-muted mb-1" style="font-size: 0.8rem; text-transform: uppercase;">1. Entidad (Municipalidad)</label>
  48 + <div class="input-group mb-2">
  49 + <input type="text" id="municipioSearch" class="form-control" placeholder="Digite código o nombre del municipio..." autocomplete="off">
  50 + <div class="input-group-append">
  51 + <div class="input-group-text">
  52 + <span class="fas fa-university"></span>
27 </div> 53 </div>
28 - <div class="form-group">  
29 - <label>Usuario</label>  
30 - <input type="text" id="username" placeholder="operador" required> 54 + </div>
  55 + </div>
  56 + <div class="form-group mb-3 position-relative">
  57 + <select id="entidad" class="form-control" required size="4" style="height: 110px; cursor: pointer; font-size: 0.9rem;">
  58 + <option value="" disabled>Cargando municipios...</option>
  59 + </select>
  60 + <div id="loader-entidades" class="text-center p-2" style="position: absolute; top:0; left:0; width:100%; height:100%; background:white; display:none;">
  61 + <div class="spinner-border spinner-border-sm text-primary"></div>
  62 + </div>
  63 + </div>
  64 +
  65 + <label class="text-xs text-muted mb-1" style="font-size: 0.8rem; text-transform: uppercase;">2. Credenciales</label>
  66 + <div class="input-group mb-3">
  67 + <input type="text" id="username" class="form-control" placeholder="Usuario (ej. operador)" required autocomplete="username">
  68 + <div class="input-group-append">
  69 + <div class="input-group-text">
  70 + <span class="fas fa-user"></span>
31 </div> 71 </div>
32 - <div class="form-group">  
33 - <label>Contraseña</label>  
34 - <input type="password" id="password" required> 72 + </div>
  73 + </div>
  74 + <div class="input-group mb-3">
  75 + <input type="password" id="password" class="form-control" placeholder="Contraseña SIGEM" required autocomplete="current-password">
  76 + <div class="input-group-append">
  77 + <div class="input-group-text">
  78 + <span class="fas fa-lock"></span>
35 </div> 79 </div>
36 - <button type="submit" class="login-btn">Acceder al Sistema</button>  
37 - <div id="error-msg"></div>  
38 - </form> 80 + </div>
  81 + </div>
  82 +
  83 + <div id="error-msg" class="alert alert-danger p-2 mb-3 text-center" style="display:none; font-size: 13px;"></div>
  84 +
  85 + <div class="row mt-4">
  86 + <div class="col-12">
  87 + <button type="submit" class="btn btn-primary btn-block font-weight-bold" id="btn-login" style="padding: 10px;">
  88 + <span id="btn-text">INICIAR SESIÓN</span>
  89 + <div class="spinner-border spinner-border-sm text-light" id="spinner" role="status" style="display:none; margin: auto;"></div>
  90 + </button>
  91 + </div>
  92 + </div>
  93 + </form>
39 </div> 94 </div>
  95 + </div>
  96 +</div>
  97 +
  98 +<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  99 +<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
  100 +
  101 +<script>
  102 + const searchInput = document.getElementById('municipioSearch');
  103 + const selectEntidad = document.getElementById('entidad');
  104 + const loader = document.getElementById('loader-entidades');
  105 + let allEntidades = [];
  106 +
  107 + // Cargar Entidades desde la Base de Datos Remota (.254)
  108 + async function loadEntidades() {
  109 + loader.style.display = 'block';
  110 + try {
  111 + const response = await fetch('/gis-geoserver/api/auth/entidades');
  112 + if (!response.ok) throw new Error("Error al obtener entidades");
  113 + allEntidades = await response.json();
  114 + renderOptions(allEntidades);
  115 + } catch (error) {
  116 + console.error(error);
  117 + selectEntidad.innerHTML = '<option value="" disabled>Error al cargar municipios</option>';
  118 + } finally {
  119 + loader.style.display = 'none';
  120 + }
  121 + }
  122 +
  123 + function renderOptions(data) {
  124 + selectEntidad.innerHTML = '';
  125 + data.forEach(ent => {
  126 + const opt = document.createElement('option');
  127 + opt.value = ent.entidad;
  128 + opt.textContent = `${ent.entidad} - ${ent.nombre}`;
  129 + selectEntidad.appendChild(opt);
  130 + });
  131 + if (data.length > 0) selectEntidad.selectedIndex = 0;
  132 + }
  133 +
  134 + searchInput.addEventListener('input', (e) => {
  135 + const text = e.target.value.toLowerCase();
  136 + const filtered = allEntidades.filter(ent =>
  137 + ent.entidad.toString().includes(text) ||
  138 + ent.nombre.toLowerCase().includes(text)
  139 + );
  140 + renderOptions(filtered);
  141 +
  142 + // Si el usuario escribió un código exacto que existe, seleccionarlo
  143 + const exactMatch = allEntidades.find(ent => ent.entidad.toString() === text);
  144 + if (exactMatch) {
  145 + selectEntidad.value = exactMatch.entidad;
  146 + }
  147 + });
40 148
41 - <script>  
42 - const entidadSelect = document.getElementById('entidad');  
43 - const loginForm = document.getElementById('loginForm'); 149 + // Carga inicial
  150 + loadEntidades();
  151 +
  152 + localStorage.removeItem('jwt');
  153 +
  154 + document.getElementById('loginForm').addEventListener('submit', function(e) {
  155 + e.preventDefault();
  156 +
  157 + const username = document.getElementById('username').value;
  158 + const password = document.getElementById('password').value;
  159 + const searchVal = searchInput.value.trim();
44 const errorMsg = document.getElementById('error-msg'); 160 const errorMsg = document.getElementById('error-msg');
  161 +
  162 + // Regla: Validar si la entidad seleccionada coincide con lo digitado o si hay una seleccionada
  163 + let entidadId = selectEntidad.value;
  164 +
  165 + // Si el buscador tiene algo pero no hay selección válida, intentamos buscar el código exacto
  166 + if (!entidadId && searchVal) {
  167 + const match = allEntidades.find(ent => ent.entidad.toString() === searchVal);
  168 + if (match) entidadId = match.entidad;
  169 + }
45 170
46 - async function loadEntidades() {  
47 - try {  
48 - const res = await fetch('/gis-geoserver/api/auth/entidades');  
49 - const list = await res.json();  
50 - list.forEach(e => {  
51 - const opt = document.createElement('option');  
52 - opt.value = e.entidad;  
53 - opt.textContent = e.nombre;  
54 - entidadSelect.appendChild(opt);  
55 - });  
56 - } catch (e) { console.error('Error al cargar entidades'); } 171 + if (!entidadId) {
  172 + errorMsg.innerText = "La MUNICIPALIDAD con código " + (searchVal || "seleccionado") + " no existe.";
  173 + errorMsg.style.display = 'block';
  174 + return;
57 } 175 }
58 176
59 - loginForm.onsubmit = async (e) => {  
60 - e.preventDefault();  
61 - errorMsg.style.display = 'none';  
62 -  
63 - const payload = {  
64 - username: document.getElementById('username').value,  
65 - password: document.getElementById('password').value,  
66 - entidad: entidadSelect.value  
67 - };  
68 -  
69 - try {  
70 - const res = await fetch('/gis-geoserver/api/auth/login', {  
71 - method: 'POST',  
72 - headers: { 'Content-Type': 'application/json' },  
73 - body: JSON.stringify(payload)  
74 - });  
75 -  
76 - if (res.ok) {  
77 - const data = await res.json();  
78 - localStorage.setItem('jwt', data.token);  
79 - localStorage.setItem('entidad', entidadSelect.value);  
80 - localStorage.setItem('map_lat', data.lat);  
81 - localStorage.setItem('map_lng', data.lng);  
82 - localStorage.setItem('map_zoom', data.zoom);  
83 - window.location.href = '/gis-geoserver/mapas';  
84 - } else {  
85 - errorMsg.innerText = 'Credenciales inválidas';  
86 - errorMsg.style.display = 'block';  
87 - }  
88 - } catch (e) {  
89 - errorMsg.innerText = 'Error al conectar con el servidor';  
90 - errorMsg.style.display = 'block';  
91 - }  
92 - };  
93 -  
94 - loadEntidades();  
95 - </script> 177 + const btnText = document.getElementById('btn-text');
  178 + const spinner = document.getElementById('spinner');
  179 +
  180 + errorMsg.style.display = 'none';
  181 + btnText.style.display = 'none';
  182 + spinner.style.display = 'block';
  183 + document.getElementById('btn-login').disabled = true;
  184 +
  185 + fetch('/gis-geoserver/api/auth/login', {
  186 + method: 'POST',
  187 + headers: { 'Content-Type': 'application/json' },
  188 + body: JSON.stringify({ username, password, entidad: entidadId })
  189 + })
  190 + .then(async response => {
  191 + const data = await response.json();
  192 + if (!response.ok) throw new Error(data.message || "Credenciales incorrectas.");
  193 + return data;
  194 + })
  195 + .then(data => {
  196 + localStorage.setItem('jwt', data.token);
  197 + localStorage.setItem('user_name', data.nombre);
  198 + localStorage.setItem('entidad', entidadId);
  199 + localStorage.setItem('entidad_nombre', data.entidadNombre);
  200 + localStorage.setItem('eslogan', data.eslogan);
  201 + localStorage.setItem('responsable', data.responsable);
  202 + localStorage.setItem('entidad_logo', data.entidadLogo);
  203 +
  204 + localStorage.setItem('map_lat', data.lat);
  205 + localStorage.setItem('map_lng', data.lng);
  206 + localStorage.setItem('map_zoom', data.zoom);
  207 +
  208 + window.location.href = "/gis-geoserver/landing";
  209 + })
  210 + .catch(error => {
  211 + errorMsg.innerText = error.message;
  212 + errorMsg.style.display = 'block';
  213 + btnText.style.display = 'block';
  214 + spinner.style.display = 'none';
  215 + document.getElementById('btn-login').disabled = false;
  216 + });
  217 + });
  218 +</script>
96 </body> 219 </body>
97 -</html>  
98 \ No newline at end of file 220 \ No newline at end of file
  221 +</html>
src/main/resources/static/mapas.html
1 <!DOCTYPE html> 1 <!DOCTYPE html>
2 <html lang="es"> 2 <html lang="es">
  3 +
3 <head> 4 <head>
4 <meta charset="UTF-8"> 5 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -13,8 +14,11 @@ @@ -13,8 +14,11 @@
13 <script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script> 14 <script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
14 <link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" /> 15 <link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" />
15 <style> 16 <style>
16 - * { box-sizing: border-box; }  
17 - body, html { 17 + * {
  18 + box-sizing: border-box;
  19 + }
  20 + body,
  21 + html {
18 height: 100%; 22 height: 100%;
19 margin: 0; 23 margin: 0;
20 font-family: 'Inter', Arial, sans-serif; 24 font-family: 'Inter', Arial, sans-serif;
@@ -22,6 +26,7 @@ @@ -22,6 +26,7 @@
22 color: #fff; 26 color: #fff;
23 overflow: hidden; 27 overflow: hidden;
24 } 28 }
  29 +
25 .header { 30 .header {
26 height: 60px; 31 height: 60px;
27 background: #1e293b; 32 background: #1e293b;
@@ -29,30 +34,47 @@ @@ -29,30 +34,47 @@
29 align-items: center; 34 align-items: center;
30 justify-content: center; 35 justify-content: center;
31 padding: 0 20px; 36 padding: 0 20px;
32 - box-shadow: 0 4px 10px rgba(0,0,0,0.5); 37 + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5);
33 font-weight: bold; 38 font-weight: bold;
34 position: relative; 39 position: relative;
35 z-index: 1001; 40 z-index: 1001;
36 } 41 }
  42 +
37 .logout-btn { 43 .logout-btn {
38 - background: rgba(255,255,255,0.1);  
39 - border: 1px solid rgba(255,255,255,0.3); 44 + background: rgba(255, 255, 255, 0.1);
  45 + border: 1px solid rgba(255, 255, 255, 0.3);
40 color: white; 46 color: white;
41 padding: 8px 16px; 47 padding: 8px 16px;
42 border-radius: 8px; 48 border-radius: 8px;
43 cursor: pointer; 49 cursor: pointer;
44 transition: all 0.2s; 50 transition: all 0.2s;
45 } 51 }
46 - .logout-btn:hover { background: #ef4444; border-color: #ef4444; }  
47 - .app-container { display: flex; height: calc(100vh - 60px); } 52 +
  53 + .logout-btn:hover {
  54 + background: #ef4444;
  55 + border-color: #ef4444;
  56 + }
  57 +
  58 + .app-container {
  59 + display: flex;
  60 + height: calc(100vh - 60px);
  61 + }
  62 +
  63 + /* MODO EMBEBIDO (Limpieza de UI) */
  64 + body.embed-mode .header { display: none !important; }
  65 + body.embed-mode .sidebar { display: none !important; }
  66 + body.embed-mode .app-container { height: 100vh !important; }
  67 + body.embed-mode #map { width: 100% !important; }
  68 +
48 .sidebar { 69 .sidebar {
49 - width: 280px; 70 + width: 170px;
50 background: #0f172a; 71 background: #0f172a;
51 - border-right: 1px solid rgba(255,255,255,0.1); 72 + border-right: 1px solid rgba(255, 255, 255, 0.1);
52 overflow-y: auto; 73 overflow-y: auto;
53 padding: 20px; 74 padding: 20px;
54 flex-shrink: 0; 75 flex-shrink: 0;
55 } 76 }
  77 +
56 .menu-title { 78 .menu-title {
57 font-size: 11px; 79 font-size: 11px;
58 color: #64748b; 80 color: #64748b;
@@ -61,6 +83,7 @@ @@ -61,6 +83,7 @@
61 margin: 25px 0 10px; 83 margin: 25px 0 10px;
62 letter-spacing: 1px; 84 letter-spacing: 1px;
63 } 85 }
  86 +
64 .menu-item { 87 .menu-item {
65 padding: 12px 16px; 88 padding: 12px 16px;
66 border-radius: 10px; 89 border-radius: 10px;
@@ -73,14 +96,178 @@ @@ -73,14 +96,178 @@
73 margin-bottom: 4px; 96 margin-bottom: 4px;
74 border: 1px solid transparent; 97 border: 1px solid transparent;
75 text-decoration: none; 98 text-decoration: none;
  99 + font-size: 11px;
  100 + }
  101 +
  102 + .menu-item:hover {
  103 + background: rgba(255, 255, 255, 0.05);
  104 + color: #fff;
76 } 105 }
77 - .menu-item:hover { background: rgba(255,255,255,0.05); color: #fff; } 106 +
78 .menu-item.active { 107 .menu-item.active {
79 background: rgba(59, 130, 246, 0.1); 108 background: rgba(59, 130, 246, 0.1);
80 color: #3b82f6; 109 color: #3b82f6;
81 border-color: rgba(59, 130, 246, 0.3); 110 border-color: rgba(59, 130, 246, 0.3);
82 } 111 }
83 - #map { flex: 1; position: relative; } 112 +
  113 + .submenu {
  114 + padding-left: 10px;
  115 + margin-bottom: 15px;
  116 + }
  117 +
  118 + #map {
  119 + flex: 1;
  120 + position: relative;
  121 + }
  122 +
  123 + /* Stats Dashboard Overlay */
  124 + .stats-grid {
  125 + display: grid;
  126 + grid-template-columns: 1fr 1fr;
  127 + gap: 10px;
  128 + margin-bottom: 20px;
  129 + }
  130 +
  131 + .stat-card {
  132 + background: rgba(255, 255, 255, 0.03);
  133 + padding: 15px;
  134 + border-radius: 12px;
  135 + border: 1px solid rgba(255, 255, 255, 0.05);
  136 + }
  137 +
  138 + .stat-label {
  139 + font-size: 11px;
  140 + color: #64748b;
  141 + text-transform: uppercase;
  142 + font-weight: 600;
  143 + }
  144 +
  145 + .stat-value {
  146 + font-size: 18px;
  147 + font-weight: 700;
  148 + color: #fff;
  149 + margin-top: 5px;
  150 + }
  151 +
  152 + /* Map Title in Header */
  153 + .header-title {
  154 + color: #fff;
  155 + font-weight: 700;
  156 + font-size: 14px;
  157 + text-transform: uppercase;
  158 + letter-spacing: 1px;
  159 + }
  160 +
  161 + /* Toggle 3D Control */
  162 + .map-controls-floating {
  163 + position: absolute;
  164 + top: 20px;
  165 + right: 20px;
  166 + z-index: 1000;
  167 + display: flex;
  168 + flex-direction: column;
  169 + gap: 10px;
  170 + }
  171 +
  172 + .map-btn {
  173 + background: rgba(15, 23, 42, 0.9);
  174 + border: 1px solid rgba(255, 255, 255, 0.1);
  175 + color: white;
  176 + padding: 10px;
  177 + border-radius: 10px;
  178 + cursor: pointer;
  179 + backdrop-filter: blur(8px);
  180 + display: flex;
  181 + align-items: center;
  182 + gap: 8px;
  183 + font-weight: 600;
  184 + font-size: 12px;
  185 + transition: all 0.2s;
  186 + }
  187 +
  188 + .map-btn:hover {
  189 + background: #1e293b;
  190 + border-color: #3b82f6;
  191 + }
  192 +
  193 + .map-btn.active {
  194 + background: #3b82f6;
  195 + border-color: #3b82f6;
  196 + }
  197 +
  198 + /* Legend */
  199 + .legend {
  200 + position: absolute;
  201 + bottom: 30px;
  202 + right: 30px;
  203 + background: rgba(15, 23, 42, 0.9);
  204 + padding: 15px;
  205 + border-radius: 12px;
  206 + border: 1px solid rgba(255, 255, 255, 0.1);
  207 + backdrop-filter: blur(10px);
  208 + color: white;
  209 + font-size: 12px;
  210 + z-index: 1000;
  211 + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5);
  212 + display: none;
  213 + }
  214 +
  215 + .legend-item {
  216 + display: flex;
  217 + align-items: center;
  218 + gap: 8px;
  219 + margin-bottom: 5px;
  220 + }
  221 +
  222 + .legend-color {
  223 + width: 12px;
  224 + height: 12px;
  225 + border-radius: 3px;
  226 + }
  227 +
  228 + /* Custom Popup Style */
  229 + .maplibregl-popup-content {
  230 + background: #1e293b;
  231 + color: white;
  232 + border-radius: 12px;
  233 + padding: 0;
  234 + border: 1px solid rgba(255, 255, 255, 0.1);
  235 + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
  236 + }
  237 +
  238 + .maplibregl-popup-tip {
  239 + border-top-color: #1e293b;
  240 + }
  241 +
  242 + .popup-header {
  243 + background: #3b82f6;
  244 + padding: 12px;
  245 + border-radius: 10px 10px 0 0;
  246 + font-weight: 700;
  247 + font-size: 13px;
  248 + text-transform: uppercase;
  249 + }
  250 +
  251 + .popup-body {
  252 + padding: 15px;
  253 + }
  254 +
  255 + .popup-stat {
  256 + margin-bottom: 10px;
  257 + }
  258 +
  259 + .popup-label {
  260 + font-size: 10px;
  261 + color: #64748b;
  262 + text-transform: uppercase;
  263 + }
  264 +
  265 + .popup-value {
  266 + font-size: 13px;
  267 + font-weight: 600;
  268 + }
  269 +
  270 + /* Leyenda de morosidad con Glassmorphism */
84 .map-legend { 271 .map-legend {
85 position: absolute; 272 position: absolute;
86 bottom: 30px; 273 bottom: 30px;
@@ -88,56 +275,124 @@ @@ -88,56 +275,124 @@
88 background: rgba(15, 23, 42, 0.85); 275 background: rgba(15, 23, 42, 0.85);
89 padding: 18px; 276 padding: 18px;
90 border-radius: 16px; 277 border-radius: 16px;
91 - border: 1px solid rgba(255,255,255,0.1); 278 + border: 1px solid rgba(255, 255, 255, 0.1);
92 color: #f1f5f9; 279 color: #f1f5f9;
93 font-size: 11px; 280 font-size: 11px;
94 z-index: 1000; 281 z-index: 1000;
95 backdrop-filter: blur(12px); 282 backdrop-filter: blur(12px);
96 - box-shadow: 0 10px 30px rgba(0,0,0,0.5); 283 + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
97 max-width: 240px; 284 max-width: 240px;
98 } 285 }
99 - .legend-item { display: flex; align-items: center; margin-bottom: 8px; gap: 10px; }  
100 - .legend-color { width: 30px; height: 12px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.1); } 286 +
  287 + .legend-item {
  288 + display: flex;
  289 + align-items: center;
  290 + margin-bottom: 8px;
  291 + gap: 10px;
  292 + }
  293 +
  294 + .legend-color {
  295 + width: 30px;
  296 + height: 12px;
  297 + border-radius: 4px;
  298 + border: 1px solid rgba(255, 255, 255, 0.1);
  299 + }
  300 +
  301 + /* Capas Switcher */
  302 + .layer-switcher {
  303 + position: absolute;
  304 + top: 20px;
  305 + right: 50px;
  306 + background: rgba(13, 17, 23, 0.9);
  307 + padding: 10px;
  308 + border-radius: 8px;
  309 + border: 1px solid rgba(255, 255, 255, 0.1);
  310 + z-index: 10;
  311 + }
101 </style> 312 </style>
102 </head> 313 </head>
  314 +
103 <body> 315 <body>
104 <div class="header"> 316 <div class="header">
105 - <div id="map-title" class="header-title">VISTA CARTOGRÁFICA GENERAL</div> 317 + <div id="map-title" class="header-title">Vista Cartográfica General</div>
106 </div> 318 </div>
107 <div class="app-container"> 319 <div class="app-container">
108 <div class="sidebar"> 320 <div class="sidebar">
109 <div id="stats-dashboard"> 321 <div id="stats-dashboard">
110 <div class="menu-title">Resumen Municipal</div> 322 <div class="menu-title">Resumen Municipal</div>
111 - <div class="stat-card" style="width: 100%; padding: 15px; background: rgba(255,255,255,0.03); border-radius: 12px; margin-bottom: 20px;">  
112 - <div class="stat-label" style="font-size: 11px; color: #64748b;">Total Lotes</div>  
113 - <div id="stat-lotes" class="stat-value" style="font-size: 24px; font-weight: 700;">...</div> 323 +
  324 + <div class="stat-card" style="width: 100%; padding: 12px 16px; border-radius: 10px; margin-bottom: 20px;">
  325 + <div class="stat-label">Total Lotes</div>
  326 + <div id="stat-lotes" class="stat-value" style="font-size: 14px;">...</div>
114 </div> 327 </div>
  328 +
  329 +
115 </div> 330 </div>
116 331
117 <div class="menu-title">Control de Gestión</div> 332 <div class="menu-title">Control de Gestión</div>
118 - <div id="menu-reset" class="menu-item active" onclick="resetMap()">Capas Base</div>  
119 - <select id="base-layer-select" style="background: #1e293b; color: white; border: 1px solid #334155; padding: 10px; width: 100%; border-radius: 8px; margin-top: 10px;">  
120 - <option value="dark">CartoDB Dark</option>  
121 - <option value="streets">OpenStreetMap</option>  
122 - <option value="satellite">Esri Satélite</option>  
123 - </select> 333 + <div id="menu-reset" class="menu-item active" onclick="resetMap()" style="width: 100%;">Capas Base</div>
  334 + <div style="margin-top: 5px; width: 100%;">
  335 + <select id="base-layer-select"
  336 + style="background: rgba(255, 255, 255, 0.05); color: #94a3b8; border: 1px solid rgba(255,255,255,0.1); padding: 8px 12px; border-radius: 6px; font-size: 10px; cursor: pointer; width: 100%; height: 35px;">
  337 + <option value="dark">CartoDB Dark</option>
  338 + <option value="streets">OpenStreetMap</option>
  339 + <option value="satellite">Esri Satélite</option>
  340 + <option value="google">Google Satélite</option>
  341 + </select>
  342 + </div>
124 343
125 <div class="menu-title">Mapas Tributarios</div> 344 <div class="menu-title">Mapas Tributarios</div>
126 - <div id="menu-ultimo-pago" class="menu-item" onclick="setHeatmap('ultimo-pago')">Por Último Pago</div>  
127 - <div id="menu-percentiles" class="menu-item" onclick="setHeatmap('percentiles')">Por Monto Adeudado</div> 345 + <div class="submenu">
  346 + <div id="menu-ultimo-pago" class="menu-item" onclick="setHeatmap('ultimo-pago')">Por Último Pago
  347 + </div>
  348 + <div id="menu-percentiles" class="menu-item" onclick="setHeatmap('percentiles')">Por Monto Adeudado</div>
  349 + <div id="menu-wms-test" class="menu-item" onclick="toggleWmsLayer()"
  350 + style="color: #fbbf24; border-top: 1px dashed #444; margin-top: 5px; font-weight: bold; display: none;">
  351 + Vista Lotes <span id="wms-status" style="font-size: 9px; opacity: 0.6;">[OFF]</span>
  352 + </div>
  353 + </div>
  354 +
  355 + <div class="menu-title">Administración</div>
  356 + <div id="btn-update-data" class="menu-item" onclick="updateMunicipalData()" style="color: #60a5fa;">
  357 + <span id="update-icon"></span> <span id="update-text">Actualizar FDW</span>
  358 + </div>
  359 +
128 </div> 360 </div>
129 361
130 <div id="map"> 362 <div id="map">
  363 + <!-- Leyenda Dinámica -->
131 <div class="map-legend" id="legend" style="display: none;"> 364 <div class="map-legend" id="legend" style="display: none;">
132 <div id="legend-content"></div> 365 <div id="legend-content"></div>
133 - <div style="font-size: 10px; margin-top: 10px; border-top: 1px solid #333; padding-top: 5px; color: #888;">Fuente: Sistema SIGEM</div> 366 + <div
  367 + style="font-size: 10px; margin-top: 10px; border-top: 1px solid #333; padding-top: 5px; color: #888;">
  368 + Fuente: Sistema SIGEM</div>
134 </div> 369 </div>
  370 +
  371 +
  372 +
135 </div> 373 </div>
136 </div> 374 </div>
137 375
138 <script> 376 <script>
  377 + // Verificar si estamos en modo embebido
  378 + const urlParams = new URLSearchParams(window.location.search);
  379 + if (urlParams.get('mode') === 'embed') {
  380 + document.body.classList.add('embed-mode');
  381 + }
  382 +
139 const entidad = localStorage.getItem('entidad') || '505'; 383 const entidad = localStorage.getItem('entidad') || '505';
140 const token = localStorage.getItem('jwt'); 384 const token = localStorage.getItem('jwt');
  385 + const geoserverBase = 'gwc/service/wmts';
  386 +
  387 + function logout() {
  388 + localStorage.clear();
  389 + window.location.href = '/gis-geoserver/login';
  390 + }
  391 +
  392 + // --- Inicialización del Mapa ---
  393 + const lat = parseFloat(localStorage.getItem('map_lat')) || -25.449;
  394 + const lng = parseFloat(localStorage.getItem('map_lng')) || -56.443;
  395 + const zoom = parseInt(localStorage.getItem('map_zoom')) || 15;
141 396
142 const map = new maplibregl.Map({ 397 const map = new maplibregl.Map({
143 container: 'map', 398 container: 'map',
@@ -148,7 +403,7 @@ @@ -148,7 +403,7 @@
148 type: 'raster', 403 type: 'raster',
149 tiles: ['https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'], 404 tiles: ['https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'],
150 tileSize: 256, 405 tileSize: 256,
151 - attribution: '&copy; CartoDB' 406 + attribution: '&copy; CartoDB - SIGEM-REGISTRO MIC/DINAPI 593-7/Julio/2016'
152 } 407 }
153 }, 408 },
154 layers: [{ 409 layers: [{
@@ -159,26 +414,36 @@ @@ -159,26 +414,36 @@
159 maxzoom: 20 414 maxzoom: 20
160 }] 415 }]
161 }, 416 },
162 - center: [parseFloat(localStorage.getItem('map_lng')) || -56.443, parseFloat(localStorage.getItem('map_lat')) || -25.449],  
163 - zoom: parseInt(localStorage.getItem('map_zoom')) || 15 417 + center: [lng, lat],
  418 + zoom: zoom,
  419 + pitch: 0,
  420 + bearing: 0,
  421 + antialias: true
164 }); 422 });
165 423
166 map.addControl(new maplibregl.NavigationControl({ position: 'bottom-left' })); 424 map.addControl(new maplibregl.NavigationControl({ position: 'bottom-left' }));
167 425
  426 + // --- Carga de Capas Vectoriales ---
168 map.on('load', () => { 427 map.on('load', () => {
169 initGisSources(); 428 initGisSources();
170 loadMunicipalStats(); 429 loadMunicipalStats();
171 }); 430 });
172 431
173 function initGisSources() { 432 function initGisSources() {
  433 + console.log("Cargando fuentes vectoriales (MVT)...");
  434 +
  435 + // Fuente de Lotes (MVT) - TMS Nativo de GeoServer
174 if (!map.getSource('lotes-mvt')) { 436 if (!map.getSource('lotes-mvt')) {
175 map.addSource('lotes-mvt', { 437 map.addSource('lotes-mvt', {
176 type: 'vector', 438 type: 'vector',
177 - tiles: [`${window.location.origin}/gis-geoserver/gwc/service/tms/1.0.0/sigem:vw_lotes_morosidad_${entidad}@XYZ-900913@pbf/{z}/{x}/{y}.pbf`], 439 + tiles: [
  440 + `${window.location.origin}/gis-geoserver/gwc/service/tms/1.0.0/sigem:vw_lotes_morosidad_${entidad}@XYZ-900913@pbf/{z}/{x}/{y}.pbf`
  441 + ],
178 scheme: 'tms' 442 scheme: 'tms'
179 }); 443 });
180 } 444 }
181 445
  446 + // Capa de Lotes (2D) - Visibilidad Inicial Mejorada
182 if (!map.getLayer('lotes-layer')) { 447 if (!map.getLayer('lotes-layer')) {
183 map.addLayer({ 448 map.addLayer({
184 id: 'lotes-layer', 449 id: 'lotes-layer',
@@ -186,21 +451,116 @@ @@ -186,21 +451,116 @@
186 source: 'lotes-mvt', 451 source: 'lotes-mvt',
187 'source-layer': `vw_lotes_morosidad_${entidad}`, 452 'source-layer': `vw_lotes_morosidad_${entidad}`,
188 paint: { 453 paint: {
189 - 'fill-color': 'rgba(59, 130, 246, 0.1)',  
190 - 'fill-outline-color': 'rgba(255, 255, 255, 0.3)' 454 + 'fill-color': 'rgba(59, 130, 246, 0.1)', // Azul tenue inicial
  455 + 'fill-outline-color': 'rgba(255, 255, 255, 0.3)' // Borde blanco visible
  456 + }
  457 + });
  458 + }
  459 +
  460 + // Fuente de Mejoras (MVT) - TMS Nativo de GeoServer
  461 + if (!map.getSource('mejoras-mvt')) {
  462 + map.addSource('mejoras-mvt', {
  463 + type: 'vector',
  464 + tiles: [
  465 + `${window.location.origin}/gis-geoserver/gwc/service/tms/1.0.0/sigem:e${entidad}_mejoras@XYZ-900913@pbf/{z}/{x}/{y}.pbf`
  466 + ],
  467 + scheme: 'tms'
  468 + });
  469 + }
  470 +
  471 + // [PRUEBA CONTROLADA] Fuente Raster WMS (Renderizado en Servidor)
  472 + if (!map.getSource('lotes-wms')) {
  473 + map.addSource('lotes-wms', {
  474 + 'type': 'raster',
  475 + 'tiles': [
  476 + `${window.location.origin}/gis-geoserver/sigem/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=sigem:vw_lotes_morosidad_${entidad}&STYLES=morosidad_style_wms&FORMAT=image/png&TRANSPARENT=TRUE&WIDTH=256&HEIGHT=256&SRS=EPSG:3857&BBOX={bbox-epsg-3857}`
  477 + ],
  478 + 'tileSize': 256
  479 + });
  480 + }
  481 +
  482 + if (!map.getLayer('lotes-wms-layer')) {
  483 + map.addLayer({
  484 + 'id': 'lotes-wms-layer',
  485 + 'type': 'raster',
  486 + 'source': 'lotes-wms',
  487 + 'paint': { 'raster-opacity': 0.8 },
  488 + 'layout': { 'visibility': 'none' }
  489 + }); // Agregamos al final
  490 + }
  491 +
  492 + // Capa de Mejoras (Inicialmente oculta o 2D)
  493 + if (!map.getLayer('mejoras-layer')) {
  494 + map.addLayer({
  495 + id: 'mejoras-layer',
  496 + type: 'fill',
  497 + source: 'mejoras-mvt',
  498 + 'source-layer': `e${entidad}_mejoras`,
  499 + paint: {
  500 + 'fill-color': 'rgba(59, 130, 246, 0.2)',
  501 + 'fill-outline-color': 'rgba(59, 130, 246, 0.5)'
191 } 502 }
192 }); 503 });
193 } 504 }
194 } 505 }
195 506
  507 + // --- Lógica de Capas Base (Estricto Fondo Nativo) ---
  508 + const baseConfig = {
  509 + 'dark': { url: 'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png', sourceMax: 20 },
  510 + 'streets': { url: 'https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png', sourceMax: 20 },
  511 + 'satellite': { url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', sourceMax: 17 },
  512 + 'google': { url: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', sourceMax: 21 }
  513 + };
  514 +
  515 + document.getElementById('base-layer-select').addEventListener('change', (e) => {
  516 + const config = baseConfig[e.target.value];
  517 +
  518 + if (map.getLayer('basemap')) map.removeLayer('basemap');
  519 + if (map.getSource('raster-tiles')) map.removeSource('raster-tiles');
  520 +
  521 + map.addSource('raster-tiles', {
  522 + type: 'raster',
  523 + tiles: [config.url],
  524 + tileSize: 256,
  525 + maxzoom: config.sourceMax,
  526 + attribution: 'SIGEM-REGISTRO MIC/DINAPI 593-7/Julio/2016'
  527 + });
  528 +
  529 + // Empujar el fondo debajo de todas las capas existentes (cero interferencia visual)
  530 + let zIndexFloor = null;
  531 + const currentLayers = map.getStyle().layers;
  532 + if (currentLayers.length > 0) {
  533 + zIndexFloor = currentLayers[0].id;
  534 + }
  535 +
  536 + map.addLayer({
  537 + id: 'basemap',
  538 + type: 'raster',
  539 + source: 'raster-tiles',
  540 + minzoom: 0, // Dibuja desde el espacio
  541 + maxzoom: 23 // Dibuja hasta nivel vecinal, reciclando loseta gracias a sourceMax
  542 + }, zIndexFloor);
  543 + });
  544 +
  545 + // Lógica de 3D eliminada por requerimiento de usuario
  546 +
  547 +
  548 + // --- Mapa de Calor (Heatmap) via Expresiones ---
196 function setHeatmap(type) { 549 function setHeatmap(type) {
197 document.querySelectorAll('.menu-item').forEach(el => el.classList.remove('active')); 550 document.querySelectorAll('.menu-item').forEach(el => el.classList.remove('active'));
  551 + const titleEl = document.getElementById('map-title');
198 const legendEl = document.getElementById('legend'); 552 const legendEl = document.getElementById('legend');
199 const legendContent = document.getElementById('legend-content'); 553 const legendContent = document.getElementById('legend-content');
200 legendEl.style.display = 'block'; 554 legendEl.style.display = 'block';
201 555
202 if (type === 'ultimo-pago') { 556 if (type === 'ultimo-pago') {
203 document.getElementById('menu-ultimo-pago').classList.add('active'); 557 document.getElementById('menu-ultimo-pago').classList.add('active');
  558 + titleEl.textContent = 'Morosidad - Último Pago';
  559 + map.setLayoutProperty('lotes-layer', 'visibility', 'visible');
  560 +
  561 + // Mostrar botón de prueba WMS solo en este modo
  562 + document.getElementById('menu-wms-test').style.display = 'block';
  563 +
204 legendContent.innerHTML = ` 564 legendContent.innerHTML = `
205 <strong style="color: #60a5fa;">ÚLTIMO PAGO</strong><br><br> 565 <strong style="color: #60a5fa;">ÚLTIMO PAGO</strong><br><br>
206 <div class="legend-item"><div class="legend-color" style="background: #6b9070;"></div> 2026</div> 566 <div class="legend-item"><div class="legend-color" style="background: #6b9070;"></div> 2026</div>
@@ -209,27 +569,186 @@ @@ -209,27 +569,186 @@
209 <div class="legend-item"><div class="legend-color" style="background: #f08060;"></div> 2023</div> 569 <div class="legend-item"><div class="legend-color" style="background: #f08060;"></div> 2023</div>
210 <div class="legend-item"><div class="legend-color" style="background: #d05660;"></div> 2022</div> 570 <div class="legend-item"><div class="legend-color" style="background: #d05660;"></div> 2022</div>
211 <div class="legend-item"><div class="legend-color" style="background: #a91d1d;"></div> 2021</div> 571 <div class="legend-item"><div class="legend-color" style="background: #a91d1d;"></div> 2021</div>
212 - <div class="legend-item"><div class="legend-color" style="background: #64748b;"></div> SIN DEUDA</div> 572 + <div class="legend-item"><div class="legend-color" style="background: #64748b;"></div> SIN DEUDA / PRESCRIPTAS</div>
213 `; 573 `;
  574 +
214 map.setPaintProperty('lotes-layer', 'fill-color', [ 575 map.setPaintProperty('lotes-layer', 'fill-color', [
215 - 'step', ['to-number', ['get', 'ultimo_pago'], 0],  
216 - '#a91d1d', 2021, '#a91d1d', 2022, '#d05660', 2023, '#f08060', 2024, '#ffd966', 2025, '#b5c47a', 2026, '#6b9070' 576 + 'step',
  577 + ['to-number', ['get', 'ultimo_pago'], 0],
  578 + '#a91d1d', // < 2021
  579 + 2021, '#a91d1d',
  580 + 2022, '#d05660',
  581 + 2023, '#f08060',
  582 + 2024, '#ffd966',
  583 + 2025, '#b5c47a',
  584 + 2026, '#6b9070'
217 ]); 585 ]);
218 } else if (type === 'percentiles') { 586 } else if (type === 'percentiles') {
219 document.getElementById('menu-percentiles').classList.add('active'); 587 document.getElementById('menu-percentiles').classList.add('active');
220 - legendContent.innerHTML = `<strong style="color: #60a5fa;">MONTO ADEUDADO</strong><br><br>...`; 588 + titleEl.textContent = 'Morosidad - Monto Adeudado';
  589 + map.setLayoutProperty('lotes-layer', 'visibility', 'visible');
  590 +
  591 + // Ocultar botón de prueba WMS
  592 + document.getElementById('menu-wms-test').style.display = 'none';
  593 +
  594 + legendContent.innerHTML = `
  595 + <strong style="color: #60a5fa;">MONTO ADEUDADO</strong><br><br>
  596 + <div class="legend-item"><div class="legend-color" style="background: #a91d1d;"></div> > 2.134.819 Gs</div>
  597 + <div class="legend-item"><div class="legend-color" style="background: #f08060;"></div> entre 2.134.819 y 1.231.876 Gs</div>
  598 + <div class="legend-item"><div class="legend-color" style="background: #ffd966;"></div> entre 1.231.876 y 718.984 Gs</div>
  599 + <div class="legend-item"><div class="legend-color" style="background: #b5c47a;"></div> entre 718.984 y 355.628 Gs</div>
  600 + <div class="legend-item"><div class="legend-color" style="background: #6b9070;"></div> <= 355.628 Gs</div>
  601 + <div class="legend-item"><div class="legend-color" style="background: #64748b;"></div> NO REGISTRADOS</div>
  602 + `;
  603 +
221 map.setPaintProperty('lotes-layer', 'fill-color', [ 604 map.setPaintProperty('lotes-layer', 'fill-color', [
222 - 'step', ['to-number', ['get', 'trb_total_deuda'], 0],  
223 - '#6b9070', 355629, '#b5c47a', 718985, '#ffd966', 1231877, '#f08060', 2134820, '#a91d1d' 605 + 'step',
  606 + ['to-number', ['get', 'trb_total_deuda'], 0],
  607 + '#6b9070', // Verde Oscuro: <= 355.628 (fallback si es 0)
  608 + 355629, '#b5c47a', // Verde Claro: 355.629 - 718.984
  609 + 718985, '#ffd966', // Amarillo: 718.985 - 1.231.876
  610 + 1231877, '#f08060', // Naranja: 1.231.877 - 2.134.819
  611 + 2134820, '#a91d1d' // Rojo: > 2.134.819
224 ]); 612 ]);
225 } 613 }
  614 + // Aseguramos que el borde sea visible en modo heatmap
  615 + map.setPaintProperty('lotes-layer', 'fill-outline-color', 'rgba(255, 255, 255, 0.4)');
226 } 616 }
227 617
228 function resetMap() { 618 function resetMap() {
229 document.querySelectorAll('.menu-item').forEach(el => el.classList.remove('active')); 619 document.querySelectorAll('.menu-item').forEach(el => el.classList.remove('active'));
230 document.getElementById('menu-reset').classList.add('active'); 620 document.getElementById('menu-reset').classList.add('active');
  621 + document.getElementById('map-title').textContent = 'Vista Cartográfica General';
231 document.getElementById('legend').style.display = 'none'; 622 document.getElementById('legend').style.display = 'none';
232 - map.setPaintProperty('lotes-layer', 'fill-color', 'rgba(59, 130, 246, 0.1)'); 623 + map.setPaintProperty('lotes-layer', 'fill-color', 'rgba(255, 255, 255, 0.05)');
  624 +
  625 + // Ocultamos WMS si está activo al resetear
  626 + if(map.getLayer('lotes-wms-layer')) {
  627 + map.setLayoutProperty('lotes-wms-layer', 'visibility', 'none');
  628 + document.getElementById('wms-status').innerText = '[OFF]';
  629 + document.getElementById('menu-wms-test').classList.remove('active');
  630 + }
  631 + // Ocultar botón de prueba WMS al resetear
  632 + document.getElementById('menu-wms-test').style.display = 'none';
  633 + }
  634 +
  635 + // --- Control de Prueba Controlada WMS ---
  636 + function toggleWmsLayer() {
  637 + const entidad = localStorage.getItem('entidad') || '505';
  638 +
  639 + // Si la capa no existe, la creamos
  640 + if (!map.getSource('lotes-wms')) {
  641 + map.addSource('lotes-wms', {
  642 + 'type': 'raster',
  643 + 'tiles': [
  644 + `${window.location.origin}/gis-geoserver/sigem/wms?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=sigem:vw_lotes_morosidad_${entidad}&STYLES=morosidad_style_wms&FORMAT=image/png&TRANSPARENT=TRUE&WIDTH=256&HEIGHT=256&SRS=EPSG:3857&BBOX={bbox-epsg-3857}`
  645 + ],
  646 + 'tileSize': 256
  647 + });
  648 +
  649 + map.addLayer({
  650 + 'id': 'lotes-wms-layer',
  651 + 'type': 'raster',
  652 + 'source': 'lotes-wms',
  653 + 'layout': { 'visibility': 'none' },
  654 + 'paint': { 'raster-opacity': 0.8 }
  655 + }, 'lotes-layer'); // Debajo de la capa de interacción MVT
  656 + }
  657 +
  658 + const isVisible = map.getLayoutProperty('lotes-wms-layer', 'visibility') === 'visible';
  659 + const nextVisibility = isVisible ? 'none' : 'visible';
  660 + const statusEl = document.getElementById('wms-status');
  661 + const menuEl = document.getElementById('menu-wms-test');
  662 +
  663 + map.setLayoutProperty('lotes-wms-layer', 'visibility', nextVisibility);
  664 +
  665 + if (nextVisibility === 'visible') {
  666 + statusEl.innerText = '[ON]';
  667 + menuEl.classList.add('active');
  668 + // Opcional: Ocultar MVT para mejor rendimiento visual en PNG Full
  669 + // map.setLayoutProperty('lotes-layer', 'visibility', 'none');
  670 + } else {
  671 + statusEl.innerText = '[OFF]';
  672 + menuEl.classList.remove('active');
  673 + map.setLayoutProperty('lotes-layer', 'visibility', 'visible');
  674 + }
  675 + }
  676 +
  677 + // --- Popups Instantáneos (MVT Data) ---
  678 + map.on('click', 'lotes-layer', (e) => {
  679 + const props = e.features[0].properties;
  680 + const content = `
  681 + <div class="popup-header">ESTADO DE CUENTA: ${props.ccc || 'N/A'}</div>
  682 + <div class="popup-body">
  683 + <div class="popup-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
  684 + <div class="popup-stat">
  685 + <div class="popup-label">Nro Ficha</div>
  686 + <div class="popup-value">${props.inm_ficha || '0'}</div>
  687 + </div>
  688 + <div class="popup-stat">
  689 + <div class="popup-label">Cta. Catastral</div>
  690 + <div class="popup-value" style="font-size: 11px;">${props.inm_ctacatastral || 'N/A'}</div>
  691 + </div>
  692 + </div>
  693 + <hr style="opacity: 0.1; margin: 10px 0;">
  694 + <div class="popup-stat">
  695 + <div class="popup-label">Total Adeudado</div>
  696 + <div class="popup-value" style="color: #f87171; font-size: 16px;">Gs. ${props.trb_total_deuda ? parseFloat(props.trb_total_deuda).toLocaleString('es-PY') : '0'}</div>
  697 + </div>
  698 + <div class="popup-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;">
  699 + <div class="popup-stat">
  700 + <div class="popup-label">Total Pagado</div>
  701 + <div class="popup-value" style="color: #10b981;">Gs. ${props.trb_total_pago ? parseFloat(props.trb_total_pago).toLocaleString('es-PY') : '0'}</div>
  702 + </div>
  703 + <div class="popup-stat">
  704 + <div class="popup-label">Último Pago</div>
  705 + <div class="popup-value">${props.ultimo_pago || 'Nunca'}</div>
  706 + </div>
  707 + </div>
  708 + <button class="logout-btn" style="width: 100%; margin-top: 15px; font-size: 11px; background: #60a5fa; color: #fff;">📄 Ver Detalles SIGEM</button>
  709 + </div>
  710 + `;
  711 +
  712 + new maplibregl.Popup()
  713 + .setLngLat(e.lngLat)
  714 + .setHTML(content)
  715 + .addTo(map);
  716 + });
  717 +
  718 + map.on('mouseenter', 'lotes-layer', () => map.getCanvas().style.cursor = 'pointer');
  719 + map.on('mouseleave', 'lotes-layer', () => map.getCanvas().style.cursor = '');
  720 +
  721 + async function updateMunicipalData() {
  722 + const btn = document.getElementById('btn-update-data');
  723 + const icon = document.getElementById('update-icon');
  724 + const text = document.getElementById('update-text');
  725 +
  726 + btn.style.pointerEvents = 'none';
  727 + btn.style.opacity = '0.5';
  728 + text.innerText = 'Procesando FDW...';
  729 + icon.classList.add('rotating'); // Podríamos añadir CSS para rotar
  730 +
  731 + try {
  732 + const res = await fetch(`/gis-geoserver/api/admin/fdw/update/${entidad}`, {
  733 + method: 'POST',
  734 + headers: { 'Authorization': `Bearer ${token}` }
  735 + });
  736 + const data = await res.json();
  737 +
  738 + if (data.success) {
  739 + alert("✅ Éxito: " + data.message);
  740 + location.reload(); // Recargamos para ver los cambios
  741 + } else {
  742 + alert("❌ Error: " + data.message);
  743 + }
  744 + } catch (err) {
  745 + console.error("Error en update:", err);
  746 + alert("❌ Error de conexión con el servidor de administración.");
  747 + } finally {
  748 + btn.style.pointerEvents = 'auto';
  749 + btn.style.opacity = '1';
  750 + text.innerText = 'Actualizar FDW';
  751 + }
233 } 752 }
234 753
235 async function loadMunicipalStats() { 754 async function loadMunicipalStats() {
@@ -239,8 +758,10 @@ @@ -239,8 +758,10 @@
239 }); 758 });
240 const data = await res.json(); 759 const data = await res.json();
241 document.getElementById('stat-lotes').innerText = data.total_lotes.toLocaleString(); 760 document.getElementById('stat-lotes').innerText = data.total_lotes.toLocaleString();
242 - } catch (e) { console.error(e); } 761 + document.getElementById('stat-morosos').innerText = data.lotes_con_deuda.toLocaleString();
  762 + } catch (err) { console.error("Error stats:", err); }
243 } 763 }
244 </script> 764 </script>
245 </body> 765 </body>
  766 +
246 </html> 767 </html>
247 \ No newline at end of file 768 \ No newline at end of file
test_dash_matches.sql 0 → 100644
  1 +-- Verificar si hay matches exitosos con registros que tenían guiones
  2 +SELECT
  3 + lot.snc_cuenta,
  4 + liq.inm_ctacatastral as fdw_original,
  5 + REPLACE(liq.inm_ctacatastral, '-', '') as fdw_limpio
  6 +FROM public.e505_lotes_activos lot
  7 +JOIN fdw_505.v_liq_entidad_totalxobjeto liq ON lot.snc_cuenta = REPLACE(liq.inm_ctacatastral, '-', '')
  8 +WHERE liq.inm_ctacatastral LIKE '%-%'
  9 +LIMIT 10;
test_morosidad_505.sql 0 → 100644
  1 +SELECT snc_cuenta, ccc, inm_ctacatastral, trb_total_deuda, trb_total_pago
  2 +FROM public.vw_lotes_morosidad_505
  3 +WHERE trb_total_deuda > 0
  4 +LIMIT 10;
truncate_505.sql 0 → 100644
  1 +-- Limpieza Integral para re-migración controlada (505)
  2 +TRUNCATE TABLE public.e505_lotes_activos RESTART IDENTITY;
update_morosidad_505.sql
1 --- Punto 1: Actualización de la Vista Maestra de Morosidad con Filtro de Entidad 1 +-- Punto 1: Actualización de la Vista Maestra de Morosidad (Regla 23 & 30)
  2 +DROP VIEW IF EXISTS public.vw_lotes_morosidad_505 CASCADE;
2 CREATE OR REPLACE VIEW public.vw_lotes_morosidad_505 AS 3 CREATE OR REPLACE VIEW public.vw_lotes_morosidad_505 AS
3 SELECT 4 SELECT
4 - l.cartodb_id, l.fid, l.dpto, l.dist, l.padron, l.zona, l.mz, l.lote,  
5 - l.shape_area, l.shape_len, l.ccc, l.obs, l.cc_lote, l.cc_man, l.layer,  
6 - l.borrado, l.geom,  
7 - m.inm_ficha, m.inm_ctacatastral, m.trb_total_deuda, m.trb_total_pago, m.ultimo_pago  
8 -FROM public.e505_lotes_conccc l  
9 -LEFT JOIN fdw_505.v_liq_entidad_totalxobjeto m ON l.ccc::text = m.inm_ctacatastral::text AND m.entidad = '505'; 5 + lot.*,
  6 + liq.inm_ficha,
  7 + liq.inm_ctacatastral,
  8 + liq.trb_tributo,
  9 + liq.trb_total_deuda,
  10 + liq.trb_total_pago,
  11 + liq.ultimo_pago
  12 +FROM public.e505_lotes_activos lot
  13 +LEFT JOIN fdw_505.v_liq_entidad_totalxobjeto liq ON lot.snc_cuenta = REPLACE(liq.inm_ctacatastral, '-', '') ;
10 14
11 -- Punto 2: Creación de la Vista de Percentiles Filtrada por Entidad (Nuevo Estándar) 15 -- Punto 2: Creación de la Vista de Percentiles Filtrada por Entidad (Nuevo Estándar)
12 CREATE OR REPLACE VIEW public.vw_percentiles_505 AS 16 CREATE OR REPLACE VIEW public.vw_percentiles_505 AS
urban_record_dump.sql 0 → 100644
  1 +-- Volcado vertical específico para Zona Urbana (505)
  2 +SELECT * FROM public.e505_lotes_activos WHERE tipo_cuenta = 0 ORDER BY id ASC LIMIT 5;
verify_oviedo_master.sql 0 → 100644
  1 +-- Verificación de Oviedo tras Ingesta Nacional
  2 +SELECT
  3 + dpto,
  4 + dist,
  5 + count(*) as total_lotes,
  6 + count(snc_cuenta) as con_cuenta_limpia
  7 +FROM public.snc_raw_lotes_activos
  8 +WHERE dpto = 'F' AND dist = 1
  9 +GROUP BY dpto, dist;
verify_prefix_21.sql 0 → 100644
  1 +-- 1. ANALISIS DE PREFIJOS EN CUENTAS CON GUIONES
  2 +SELECT
  3 + LEFT(inm_ctacatastral, 3) as prefijo,
  4 + count(*) as total_registros,
  5 + min(inm_ctacatastral) as ejemplo_min,
  6 + max(inm_ctacatastral) as ejemplo_max
  7 +FROM fdw_505.v_liq_entidad_totalxobjeto
  8 +WHERE trb_tributo = 'INM' AND inm_ctacatastral LIKE '%-%'
  9 +GROUP BY LEFT(inm_ctacatastral, 3)
  10 +ORDER BY total_registros DESC;
  11 +
  12 +-- 2. VERIFICAR SI HAY ALGUN REGISTRO CON GUION QUE NO EMPIECE CON '21-'
  13 +SELECT
  14 + inm_ctacatastral,
  15 + inm_ficha
  16 +FROM fdw_505.v_liq_entidad_totalxobjeto
  17 +WHERE trb_tributo = 'INM' AND inm_ctacatastral LIKE '%-%' AND inm_ctacatastral NOT LIKE '21-%'
  18 +LIMIT 10;
GitLab Appliance - Powered by TurnKey Linux