Commit 3868a9a47d991e803db71f458417a106652a0520

Authored by Antigravity AI
1 parent 34c9927f

Revertir servidor a estado funcional anterior (480b16a1)

Showing 57 changed files with 213 additions and 1440 deletions
GIS-GEOSERVER-REGLAS.md
@@ -2,7 +2,6 @@ @@ -2,7 +2,6 @@
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.  
6 5
7 Regla 1. El ambiente de desarrollo y compilación se encuentra en el 192.168.1.123. 6 Regla 1. El ambiente de desarrollo y compilación se encuentra en el 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. 7 El JENKINS a usar está en este servidor. Los comandos del jenkins se ejecutan en el servidor 192.168.1.123.
@@ -87,7 +86,6 @@ Regla 23. Columnas de Unión (Joins). Standard SNC. @@ -87,7 +86,6 @@ Regla 23. Columnas de Unión (Joins). Standard SNC.
87 Para toda vista de unión con el FDW, se debe utilizar la columna `snc_cuenta` (limpia) contra `REPLACE(liq.inm_ctacatastral, '-', '')`. 86 Para toda vista de unión con el FDW, se debe utilizar la columna `snc_cuenta` (limpia) contra `REPLACE(liq.inm_ctacatastral, '-', '')`.
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: 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:
89 ```sql 88 ```sql
90 -DROP VIEW IF EXISTS public.vw_lotes_morosidad_XXX CASCADE;  
91 CREATE OR REPLACE VIEW public.vw_lotes_morosidad_XXX AS 89 CREATE OR REPLACE VIEW public.vw_lotes_morosidad_XXX AS
92 SELECT 90 SELECT
93 lot.*, 91 lot.*,
@@ -116,21 +114,13 @@ Almacenamiento en carpeta cronológica dentro de /publico/. @@ -116,21 +114,13 @@ Almacenamiento en carpeta cronológica dentro de /publico/.
116 114
117 Solo bajo autorización del usuario. 115 Solo bajo autorización del usuario.
118 116
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. 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.
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 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
135 125
136 Regla 27. Optimización y Cache GeoWebCache (GWC): 126 Regla 27. Optimización y Cache GeoWebCache (GWC):
@@ -144,13 +134,3 @@ La inserción en la base de datos se realizará mediante el uso directo de ST_Ge @@ -144,13 +134,3 @@ La inserción en la base de datos se realizará mediante el uso directo de ST_Ge
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. 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.
145 Las columnas de las tablas eXXX_lotes_activos deberá tener todas las columnas del SNC. 135 Las columnas de las tablas eXXX_lotes_activos deberá tener todas las columnas del SNC.
146 136
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 deleted
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 deleted
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 deleted
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 deleted
1 -SELECT * FROM public.snc_catalog_mapping WHERE entidad_id = '505';  
check_bounds_505.sql deleted
1 -SELECT entidad, boundno, boundse, latlong, zoom  
2 -FROM public.entidades  
3 -WHERE entidad = 505;  
check_cols_505.sql deleted
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 deleted
1 -SELECT tipo_cuenta, count(*) FROM public.e505_lotes_activos GROUP BY tipo_cuenta;  
check_map_colors_505.sql deleted
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 deleted
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 deleted
1 -SELECT count(*) as total_lotes_locales FROM public.snc_raw_lotes_activos;  
check_oviedo_local_load.sql deleted
1 -SELECT count(*) as total_lotes_oviedo FROM public.e505_lotes_activos;  
check_oviedo_mapping.sql deleted
1 -SELECT * FROM public.snc_catalog_mapping WHERE entidad_id = '505';  
check_view_def.sql deleted
1 -SELECT pg_get_viewdef('public.vw_lotes_morosidad_505', true);  
check_vinculacion_505.sql deleted
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 deleted
1 -SELECT count(*) as total_lotes  
2 -FROM public.e505_lotes_activos;  
create_master_snc_table.sql deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
1 -SELECT count(*) as total, count(NULLIF(snc_cuenta, '')) as con_cuenta FROM public.e505_lotes_activos;  
final_logic_verification.sql deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
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 deleted
1 -SELECT * FROM public.snc_catalog_mapping WHERE entidad_id = '505';  
src/main/java/com/sigem/gis/WebConfig.java
@@ -14,8 +14,6 @@ public class WebConfig implements WebMvcConfigurer { @@ -14,8 +14,6 @@ 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");  
19 registry.addViewController("/").setViewName("forward:/login.html"); 17 registry.addViewController("/").setViewName("forward:/login.html");
20 } 18 }
21 19
src/main/java/com/sigem/gis/controller/SncImportController.java
@@ -3,7 +3,6 @@ package com.sigem.gis.controller; @@ -3,7 +3,6 @@ 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;  
7 import org.springframework.web.bind.annotation.*; 6 import org.springframework.web.bind.annotation.*;
8 import org.springframework.web.client.RestTemplate; 7 import org.springframework.web.client.RestTemplate;
9 import java.util.*; 8 import java.util.*;
@@ -512,7 +511,7 @@ public class SncImportController { @@ -512,7 +511,7 @@ public class SncImportController {
512 System.out.println(String.format("[%d/%d] PROCESANDO ENTIDAD %s (SNC: %s-%s)", current, total, eid, 511 System.out.println(String.format("[%d/%d] PROCESANDO ENTIDAD %s (SNC: %s-%s)", current, total, eid,
513 dpto, dist)); 512 dpto, dist));
514 try { 513 try {
515 - importDistrict(eid, dpto, dist, 0, false, false); 514 + importDistrict(eid, dpto, dist, false);
516 } catch (Exception e) { 515 } catch (Exception e) {
517 System.err.println("!!! FALLO CRÍTICO EN ENTIDAD " + eid + ": " + e.getMessage()); 516 System.err.println("!!! FALLO CRÍTICO EN ENTIDAD " + eid + ": " + e.getMessage());
518 } 517 }
@@ -527,36 +526,20 @@ public class SncImportController { @@ -527,36 +526,20 @@ public class SncImportController {
527 } 526 }
528 527
529 @GetMapping("/snc/{entityId}/{dpto}/{dist}") 528 @GetMapping("/snc/{entityId}/{dpto}/{dist}")
530 - public ResponseEntity<String> importDistrict( 529 + public String importDistrict(
531 @PathVariable String entityId, 530 @PathVariable String entityId,
532 @PathVariable String dpto, 531 @PathVariable String dpto,
533 @PathVariable String dist, 532 @PathVariable String dist,
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 - } 533 + @RequestParam(defaultValue = "true") boolean processFdw) {
556 534
557 - private int importFromWfs(String tableName, String dpto, String dist, int limit) {  
558 try { 535 try {
559 - System.out.println(">>> RECUPERACIÓN WFS: " + dpto + "-" + dist + " -> Activos y Maestra"); 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)
560 String url = org.springframework.web.util.UriComponentsBuilder 543 String url = org.springframework.web.util.UriComponentsBuilder
561 .fromHttpUrl("https://www.catastro.gov.py/geoserver/ows") 544 .fromHttpUrl("https://www.catastro.gov.py/geoserver/ows")
562 .queryParam("service", "WFS") 545 .queryParam("service", "WFS")
@@ -570,137 +553,90 @@ public class SncImportController { @@ -570,137 +553,90 @@ public class SncImportController {
570 .toUriString(); 553 .toUriString();
571 554
572 Map<String, Object> response = restTemplate.getForObject(url, Map.class); 555 Map<String, Object> response = restTemplate.getForObject(url, Map.class);
573 - if (response == null) return 0; 556 + if (response == null) {
  557 + System.err.println("!!! Respuesta NULL para Entidad " + entityId);
  558 + return "ERR: Respuesta NULL del SNC";
  559 + }
574 560
575 List<Map<String, Object>> features = (List<Map<String, Object>>) response.get("features"); 561 List<Map<String, Object>> features = (List<Map<String, Object>>) response.get("features");
576 - if (features == null || features.isEmpty()) return 0; 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 + }
577 566
578 com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); 567 com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
579 List<Object[]> batchArgs = new ArrayList<>(); 568 List<Object[]> batchArgs = new ArrayList<>();
580 569
581 for (Map<String, Object> feature : features) { 570 for (Map<String, Object> feature : features) {
582 Map<String, Object> props = (Map<String, Object>) feature.get("properties"); 571 Map<String, Object> props = (Map<String, Object>) feature.get("properties");
583 - Integer tc = (Integer) props.get("tipo_cuenta");  
584 - Object shapeObj = props.get("shape") != null ? props.get("shape") : feature.get("geometry"); 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 +
585 String ccatastral = (String) props.get("ccatastral"); 579 String ccatastral = (String) props.get("ccatastral");
  580 + Integer tc = (Integer) props.get("tipo_cuenta");
586 Object padronObj = props.get("padron"); 581 Object padronObj = props.get("padron");
587 String padronStr = padronObj != null ? String.valueOf(padronObj) : ""; 582 String padronStr = padronObj != null ? String.valueOf(padronObj) : "";
588 583
  584 + // REGLA 26: Normalización Universal de Cartografía SNC (Actualizada)
589 String snc_cuenta = ""; 585 String snc_cuenta = "";
590 - if (tc != null && tc == 1) {  
591 - // URBANO (tipo_cuenta = 1): Substring(ccatastral, 4) 586 + if (tc != null && tc == 0) {
  587 + // 1. Zona Urbana (tipo_cuenta = 0): Substring(ccatastral, 4) eliminando ceros
592 if (ccatastral != null && ccatastral.length() >= 4) { 588 if (ccatastral != null && ccatastral.length() >= 4) {
593 - String cleaned = ccatastral.substring(3).replaceAll("[^a-zA-Z0-9]", "");  
594 - snc_cuenta = cleaned.replaceFirst("^0+", ""); 589 + snc_cuenta = ccatastral.substring(3).replaceAll("^0+", "").replaceAll("[^a-zA-Z0-9]", "");
  590 + } else if (!padronStr.isEmpty()) {
  591 + snc_cuenta = padronStr.replaceAll("^0+", "");
595 } 592 }
596 - } else if (tc != null && tc == 0) {  
597 - // RURAL (tipo_cuenta = 0): Padron 593 + } else if (tc != null && tc == 1) {
  594 + // 2. Zona Rural (tipo_cuenta = 1): padron::text (sin modificaciones)
598 snc_cuenta = padronStr; 595 snc_cuenta = padronStr;
599 } 596 }
600 597
601 try { 598 try {
  599 + String geomJson = mapper.writeValueAsString(shapeObj);
  600 + if (shapeObj == null)
  601 + continue;
  602 +
602 batchArgs.add(new Object[] { 603 batchArgs.add(new Object[] {
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 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
615 }); 616 });
616 - } catch (Exception e) {}  
617 - }  
618 -  
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();  
643 -  
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");  
649 -  
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 - } 617 + } catch (Exception e) {
658 } 618 }
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();  
664 } 619 }
665 - }).start();  
666 - return "OK: Proceso de Ingesta Nacional iniciado en segundo plano. Verifique logs.";  
667 - }  
668 620
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"); 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)), ?, ?)";
675 628
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); 629 + gisJdbcTemplate.batchUpdate(insertSql, batchArgs);
  630 + System.out.println("+++ EXITO: " + entityId + " -> Inyectados " + batchArgs.size() + " registros.");
  631 +
  632 + if (processFdw) {
  633 + processFdwAndViews(entityId);
696 } 634 }
697 635
698 - System.out.println("+++ EXITO LOCAL: Ubicados " + rows + " registros para " + tableName);  
699 - return rows; 636 + return "OK: " + entityId + " (" + features.size() + " recs)";
700 } catch (Exception e) { 637 } catch (Exception e) {
701 - System.err.println("!!! FALLO en Transferencia Local: " + e.getMessage());  
702 - e.printStackTrace();  
703 - return 0; 638 + System.err.println("!!! FALLO en Importación de " + entityId + ": " + e.getMessage());
  639 + return "ERR: " + entityId + " -> " + e.getMessage();
704 } 640 }
705 } 641 }
706 642
@@ -794,7 +730,7 @@ public class SncImportController { @@ -794,7 +730,7 @@ public class SncImportController {
794 entityId, fdwSchema)); 730 entityId, fdwSchema));
795 731
796 gisJdbcTemplate.execute("CREATE OR REPLACE VIEW public.vw_lotes_morosidad_" + entityId + " AS " + 732 gisJdbcTemplate.execute("CREATE OR REPLACE VIEW public.vw_lotes_morosidad_" + entityId + " AS " +
797 - "SELECT lot.*, liq.inm_ficha, liq.inm_ctacatastral, liq.trb_tributo, liq.trb_total_deuda, liq.trb_total_pago, liq.ultimo_pago " 733 + "SELECT lot.*, liq.inm_ficha, liq.inm_ctacatastral, liq.trb_total_deuda, liq.trb_total_pago, liq.ultimo_pago "
798 + 734 +
799 "FROM public.e" + entityId + "_lotes_activos lot " + 735 "FROM public.e" + entityId + "_lotes_activos lot " +
800 "LEFT JOIN " + fdwSchema 736 "LEFT JOIN " + fdwSchema
src/main/java/com/sigem/gis/security/SecurityConfig.java
@@ -30,7 +30,6 @@ public class SecurityConfig { @@ -30,7 +30,6 @@ 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  
34 .requestMatchers("/login.html", "/", "/mapas/**", "/mapas.html", "/login", "/error", "/landing", "/landing.html", "/widgets", "/widgets.html").permitAll() 33 .requestMatchers("/login.html", "/", "/mapas/**", "/mapas.html", "/login", "/error", "/landing", "/landing.html", "/widgets", "/widgets.html").permitAll()
35 .requestMatchers("/mapas_institucional.html").permitAll() 34 .requestMatchers("/mapas_institucional.html").permitAll()
36 .requestMatchers("/css/**", "/js/**", "/img/**", "/vendor/**").permitAll() // Recursos 35 .requestMatchers("/css/**", "/js/**", "/img/**", "/vendor/**").permitAll() // Recursos
src/main/java/com/sigem/gis/service/FdwService.java
@@ -51,8 +51,7 @@ public class FdwService { @@ -51,8 +51,7 @@ 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  
55 - // OBLIGATORIA 54 + // 2. Ejecutar comandos DDL en el servidor PostGIS (.123) - RECREACIÓN OBLIGATORIA
56 try { 55 try {
57 // Recreación del Servidor y Mapeo 56 // Recreación del Servidor y Mapeo
58 gisJdbcTemplate.execute("DROP SERVER IF EXISTS " + serverName + " CASCADE"); 57 gisJdbcTemplate.execute("DROP SERVER IF EXISTS " + serverName + " CASCADE");
@@ -75,24 +74,20 @@ public class FdwService { @@ -75,24 +74,20 @@ public class FdwService {
75 74
76 // Vista de Auditoría (MVT) - REGLA 23 75 // Vista de Auditoría (MVT) - REGLA 23
77 String viewLotesName = "vw_lotes_morosidad_" + entidadId; 76 String viewLotesName = "vw_lotes_morosidad_" + entidadId;
78 - gisJdbcTemplate.execute("DROP VIEW IF EXISTS public." + viewLotesName + " CASCADE");  
79 gisJdbcTemplate.execute(String.format( 77 gisJdbcTemplate.execute(String.format(
80 "CREATE OR REPLACE VIEW public.%s AS " + 78 "CREATE OR REPLACE VIEW public.%s AS " +
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, '-', '')", 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, '-', '')",
85 viewLotesName, tableLotes, schemaName)); 82 viewLotesName, tableLotes, schemaName));
86 83
87 // Vista PNG FULL (WMS) - REGLA 23 84 // Vista PNG FULL (WMS) - REGLA 23
88 String viewWmsName = "vw_lotes_wms_" + entidadId; 85 String viewWmsName = "vw_lotes_wms_" + entidadId;
89 - gisJdbcTemplate.execute("DROP VIEW IF EXISTS public." + viewWmsName + " CASCADE");  
90 gisJdbcTemplate.execute(String.format( 86 gisJdbcTemplate.execute(String.format(
91 "CREATE OR REPLACE VIEW public.%s AS " + 87 "CREATE OR REPLACE VIEW public.%s AS " +
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, '-', '')", 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, '-', '')",
96 viewWmsName, tableLotes, schemaName)); 91 viewWmsName, tableLotes, schemaName));
97 92
98 // 4. Sincronización con GeoServer 93 // 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">  
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> 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>
36 </head> 19 </head>
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> 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>
53 </div> 27 </div>
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> 28 + <div class="form-group">
  29 + <label>Usuario</label>
  30 + <input type="text" id="username" placeholder="operador" required>
71 </div> 31 </div>
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> 32 + <div class="form-group">
  33 + <label>Contraseña</label>
  34 + <input type="password" id="password" required>
79 </div> 35 </div>
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> 36 + <button type="submit" class="login-btn">Acceder al Sistema</button>
  37 + <div id="error-msg"></div>
  38 + </form>
94 </div> 39 </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 - });  
148 40
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(); 41 + <script>
  42 + const entidadSelect = document.getElementById('entidad');
  43 + const loginForm = document.getElementById('loginForm');
160 const errorMsg = document.getElementById('error-msg'); 44 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 - }  
170 45
171 - if (!entidadId) {  
172 - errorMsg.innerText = "La MUNICIPALIDAD con código " + (searchVal || "seleccionado") + " no existe.";  
173 - errorMsg.style.display = 'block';  
174 - return; 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'); }
175 } 57 }
176 58
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> 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>
219 </body> 96 </body>
220 -</html> 97 -</html>
  98 +</html>
221 \ No newline at end of file 99 \ No newline at end of file
src/main/resources/static/mapas.html
1 <!DOCTYPE html> 1 <!DOCTYPE html>
2 <html lang="es"> 2 <html lang="es">
3 -  
4 <head> 3 <head>
5 <meta charset="UTF-8"> 4 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -14,11 +13,8 @@ @@ -14,11 +13,8 @@
14 <script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script> 13 <script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
15 <link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" /> 14 <link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" />
16 <style> 15 <style>
17 - * {  
18 - box-sizing: border-box;  
19 - }  
20 - body,  
21 - html { 16 + * { box-sizing: border-box; }
  17 + body, html {
22 height: 100%; 18 height: 100%;
23 margin: 0; 19 margin: 0;
24 font-family: 'Inter', Arial, sans-serif; 20 font-family: 'Inter', Arial, sans-serif;
@@ -26,7 +22,6 @@ @@ -26,7 +22,6 @@
26 color: #fff; 22 color: #fff;
27 overflow: hidden; 23 overflow: hidden;
28 } 24 }
29 -  
30 .header { 25 .header {
31 height: 60px; 26 height: 60px;
32 background: #1e293b; 27 background: #1e293b;
@@ -34,47 +29,30 @@ @@ -34,47 +29,30 @@
34 align-items: center; 29 align-items: center;
35 justify-content: center; 30 justify-content: center;
36 padding: 0 20px; 31 padding: 0 20px;
37 - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.5); 32 + box-shadow: 0 4px 10px rgba(0,0,0,0.5);
38 font-weight: bold; 33 font-weight: bold;
39 position: relative; 34 position: relative;
40 z-index: 1001; 35 z-index: 1001;
41 } 36 }
42 -  
43 .logout-btn { 37 .logout-btn {
44 - background: rgba(255, 255, 255, 0.1);  
45 - border: 1px solid rgba(255, 255, 255, 0.3); 38 + background: rgba(255,255,255,0.1);
  39 + border: 1px solid rgba(255,255,255,0.3);
46 color: white; 40 color: white;
47 padding: 8px 16px; 41 padding: 8px 16px;
48 border-radius: 8px; 42 border-radius: 8px;
49 cursor: pointer; 43 cursor: pointer;
50 transition: all 0.2s; 44 transition: all 0.2s;
51 } 45 }
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 - 46 + .logout-btn:hover { background: #ef4444; border-color: #ef4444; }
  47 + .app-container { display: flex; height: calc(100vh - 60px); }
69 .sidebar { 48 .sidebar {
70 - width: 170px; 49 + width: 280px;
71 background: #0f172a; 50 background: #0f172a;
72 - border-right: 1px solid rgba(255, 255, 255, 0.1); 51 + border-right: 1px solid rgba(255,255,255,0.1);
73 overflow-y: auto; 52 overflow-y: auto;
74 padding: 20px; 53 padding: 20px;
75 flex-shrink: 0; 54 flex-shrink: 0;
76 } 55 }
77 -  
78 .menu-title { 56 .menu-title {
79 font-size: 11px; 57 font-size: 11px;
80 color: #64748b; 58 color: #64748b;
@@ -83,7 +61,6 @@ @@ -83,7 +61,6 @@
83 margin: 25px 0 10px; 61 margin: 25px 0 10px;
84 letter-spacing: 1px; 62 letter-spacing: 1px;
85 } 63 }
86 -  
87 .menu-item { 64 .menu-item {
88 padding: 12px 16px; 65 padding: 12px 16px;
89 border-radius: 10px; 66 border-radius: 10px;
@@ -96,178 +73,14 @@ @@ -96,178 +73,14 @@
96 margin-bottom: 4px; 73 margin-bottom: 4px;
97 border: 1px solid transparent; 74 border: 1px solid transparent;
98 text-decoration: none; 75 text-decoration: none;
99 - font-size: 11px;  
100 - }  
101 -  
102 - .menu-item:hover {  
103 - background: rgba(255, 255, 255, 0.05);  
104 - color: #fff;  
105 } 76 }
106 - 77 + .menu-item:hover { background: rgba(255,255,255,0.05); color: #fff; }
107 .menu-item.active { 78 .menu-item.active {
108 background: rgba(59, 130, 246, 0.1); 79 background: rgba(59, 130, 246, 0.1);
109 color: #3b82f6; 80 color: #3b82f6;
110 border-color: rgba(59, 130, 246, 0.3); 81 border-color: rgba(59, 130, 246, 0.3);
111 } 82 }
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 */ 83 + #map { flex: 1; position: relative; }
271 .map-legend { 84 .map-legend {
272 position: absolute; 85 position: absolute;
273 bottom: 30px; 86 bottom: 30px;
@@ -275,124 +88,56 @@ @@ -275,124 +88,56 @@
275 background: rgba(15, 23, 42, 0.85); 88 background: rgba(15, 23, 42, 0.85);
276 padding: 18px; 89 padding: 18px;
277 border-radius: 16px; 90 border-radius: 16px;
278 - border: 1px solid rgba(255, 255, 255, 0.1); 91 + border: 1px solid rgba(255,255,255,0.1);
279 color: #f1f5f9; 92 color: #f1f5f9;
280 font-size: 11px; 93 font-size: 11px;
281 z-index: 1000; 94 z-index: 1000;
282 backdrop-filter: blur(12px); 95 backdrop-filter: blur(12px);
283 - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); 96 + box-shadow: 0 10px 30px rgba(0,0,0,0.5);
284 max-width: 240px; 97 max-width: 240px;
285 } 98 }
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 - } 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); }
312 </style> 101 </style>
313 </head> 102 </head>
314 -  
315 <body> 103 <body>
316 <div class="header"> 104 <div class="header">
317 - <div id="map-title" class="header-title">Vista Cartográfica General</div> 105 + <div id="map-title" class="header-title">VISTA CARTOGRÁFICA GENERAL</div>
318 </div> 106 </div>
319 <div class="app-container"> 107 <div class="app-container">
320 <div class="sidebar"> 108 <div class="sidebar">
321 <div id="stats-dashboard"> 109 <div id="stats-dashboard">
322 <div class="menu-title">Resumen Municipal</div> 110 <div class="menu-title">Resumen Municipal</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> 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>
327 </div> 114 </div>
328 -  
329 -  
330 </div> 115 </div>
331 116
332 <div class="menu-title">Control de Gestión</div> 117 <div class="menu-title">Control de Gestión</div>
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> 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>
343 124
344 <div class="menu-title">Mapas Tributarios</div> 125 <div class="menu-title">Mapas Tributarios</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 - 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>
360 </div> 128 </div>
361 129
362 <div id="map"> 130 <div id="map">
363 - <!-- Leyenda Dinámica -->  
364 <div class="map-legend" id="legend" style="display: none;"> 131 <div class="map-legend" id="legend" style="display: none;">
365 <div id="legend-content"></div> 132 <div id="legend-content"></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> 133 + <div style="font-size: 10px; margin-top: 10px; border-top: 1px solid #333; padding-top: 5px; color: #888;">Fuente: Sistema SIGEM</div>
369 </div> 134 </div>
370 -  
371 -  
372 -  
373 </div> 135 </div>
374 </div> 136 </div>
375 137
376 <script> 138 <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 -  
383 const entidad = localStorage.getItem('entidad') || '505'; 139 const entidad = localStorage.getItem('entidad') || '505';
384 const token = localStorage.getItem('jwt'); 140 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;  
396 141
397 const map = new maplibregl.Map({ 142 const map = new maplibregl.Map({
398 container: 'map', 143 container: 'map',
@@ -403,7 +148,7 @@ @@ -403,7 +148,7 @@
403 type: 'raster', 148 type: 'raster',
404 tiles: ['https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'], 149 tiles: ['https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'],
405 tileSize: 256, 150 tileSize: 256,
406 - attribution: '&copy; CartoDB - SIGEM-REGISTRO MIC/DINAPI 593-7/Julio/2016' 151 + attribution: '&copy; CartoDB'
407 } 152 }
408 }, 153 },
409 layers: [{ 154 layers: [{
@@ -414,36 +159,26 @@ @@ -414,36 +159,26 @@
414 maxzoom: 20 159 maxzoom: 20
415 }] 160 }]
416 }, 161 },
417 - center: [lng, lat],  
418 - zoom: zoom,  
419 - pitch: 0,  
420 - bearing: 0,  
421 - antialias: true 162 + center: [parseFloat(localStorage.getItem('map_lng')) || -56.443, parseFloat(localStorage.getItem('map_lat')) || -25.449],
  163 + zoom: parseInt(localStorage.getItem('map_zoom')) || 15
422 }); 164 });
423 165
424 map.addControl(new maplibregl.NavigationControl({ position: 'bottom-left' })); 166 map.addControl(new maplibregl.NavigationControl({ position: 'bottom-left' }));
425 167
426 - // --- Carga de Capas Vectoriales ---  
427 map.on('load', () => { 168 map.on('load', () => {
428 initGisSources(); 169 initGisSources();
429 loadMunicipalStats(); 170 loadMunicipalStats();
430 }); 171 });
431 172
432 function initGisSources() { 173 function initGisSources() {
433 - console.log("Cargando fuentes vectoriales (MVT)...");  
434 -  
435 - // Fuente de Lotes (MVT) - TMS Nativo de GeoServer  
436 if (!map.getSource('lotes-mvt')) { 174 if (!map.getSource('lotes-mvt')) {
437 map.addSource('lotes-mvt', { 175 map.addSource('lotes-mvt', {
438 type: 'vector', 176 type: 'vector',
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 - ], 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`],
442 scheme: 'tms' 178 scheme: 'tms'
443 }); 179 });
444 } 180 }
445 181
446 - // Capa de Lotes (2D) - Visibilidad Inicial Mejorada  
447 if (!map.getLayer('lotes-layer')) { 182 if (!map.getLayer('lotes-layer')) {
448 map.addLayer({ 183 map.addLayer({
449 id: 'lotes-layer', 184 id: 'lotes-layer',
@@ -451,116 +186,21 @@ @@ -451,116 +186,21 @@
451 source: 'lotes-mvt', 186 source: 'lotes-mvt',
452 'source-layer': `vw_lotes_morosidad_${entidad}`, 187 'source-layer': `vw_lotes_morosidad_${entidad}`,
453 paint: { 188 paint: {
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)' 189 + 'fill-color': 'rgba(59, 130, 246, 0.1)',
  190 + 'fill-outline-color': 'rgba(255, 255, 255, 0.3)'
502 } 191 }
503 }); 192 });
504 } 193 }
505 } 194 }
506 195
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 ---  
549 function setHeatmap(type) { 196 function setHeatmap(type) {
550 document.querySelectorAll('.menu-item').forEach(el => el.classList.remove('active')); 197 document.querySelectorAll('.menu-item').forEach(el => el.classList.remove('active'));
551 - const titleEl = document.getElementById('map-title');  
552 const legendEl = document.getElementById('legend'); 198 const legendEl = document.getElementById('legend');
553 const legendContent = document.getElementById('legend-content'); 199 const legendContent = document.getElementById('legend-content');
554 legendEl.style.display = 'block'; 200 legendEl.style.display = 'block';
555 201
556 if (type === 'ultimo-pago') { 202 if (type === 'ultimo-pago') {
557 document.getElementById('menu-ultimo-pago').classList.add('active'); 203 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 -  
564 legendContent.innerHTML = ` 204 legendContent.innerHTML = `
565 <strong style="color: #60a5fa;">ÚLTIMO PAGO</strong><br><br> 205 <strong style="color: #60a5fa;">ÚLTIMO PAGO</strong><br><br>
566 <div class="legend-item"><div class="legend-color" style="background: #6b9070;"></div> 2026</div> 206 <div class="legend-item"><div class="legend-color" style="background: #6b9070;"></div> 2026</div>
@@ -569,186 +209,27 @@ @@ -569,186 +209,27 @@
569 <div class="legend-item"><div class="legend-color" style="background: #f08060;"></div> 2023</div> 209 <div class="legend-item"><div class="legend-color" style="background: #f08060;"></div> 2023</div>
570 <div class="legend-item"><div class="legend-color" style="background: #d05660;"></div> 2022</div> 210 <div class="legend-item"><div class="legend-color" style="background: #d05660;"></div> 2022</div>
571 <div class="legend-item"><div class="legend-color" style="background: #a91d1d;"></div> 2021</div> 211 <div class="legend-item"><div class="legend-color" style="background: #a91d1d;"></div> 2021</div>
572 - <div class="legend-item"><div class="legend-color" style="background: #64748b;"></div> SIN DEUDA / PRESCRIPTAS</div> 212 + <div class="legend-item"><div class="legend-color" style="background: #64748b;"></div> SIN DEUDA</div>
573 `; 213 `;
574 -  
575 map.setPaintProperty('lotes-layer', 'fill-color', [ 214 map.setPaintProperty('lotes-layer', 'fill-color', [
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' 215 + 'step', ['to-number', ['get', 'ultimo_pago'], 0],
  216 + '#a91d1d', 2021, '#a91d1d', 2022, '#d05660', 2023, '#f08060', 2024, '#ffd966', 2025, '#b5c47a', 2026, '#6b9070'
585 ]); 217 ]);
586 } else if (type === 'percentiles') { 218 } else if (type === 'percentiles') {
587 document.getElementById('menu-percentiles').classList.add('active'); 219 document.getElementById('menu-percentiles').classList.add('active');
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 - 220 + legendContent.innerHTML = `<strong style="color: #60a5fa;">MONTO ADEUDADO</strong><br><br>...`;
604 map.setPaintProperty('lotes-layer', 'fill-color', [ 221 map.setPaintProperty('lotes-layer', 'fill-color', [
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 222 + 'step', ['to-number', ['get', 'trb_total_deuda'], 0],
  223 + '#6b9070', 355629, '#b5c47a', 718985, '#ffd966', 1231877, '#f08060', 2134820, '#a91d1d'
612 ]); 224 ]);
613 } 225 }
614 - // Aseguramos que el borde sea visible en modo heatmap  
615 - map.setPaintProperty('lotes-layer', 'fill-outline-color', 'rgba(255, 255, 255, 0.4)');  
616 } 226 }
617 227
618 function resetMap() { 228 function resetMap() {
619 document.querySelectorAll('.menu-item').forEach(el => el.classList.remove('active')); 229 document.querySelectorAll('.menu-item').forEach(el => el.classList.remove('active'));
620 document.getElementById('menu-reset').classList.add('active'); 230 document.getElementById('menu-reset').classList.add('active');
621 - document.getElementById('map-title').textContent = 'Vista Cartográfica General';  
622 document.getElementById('legend').style.display = 'none'; 231 document.getElementById('legend').style.display = 'none';
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 - } 232 + map.setPaintProperty('lotes-layer', 'fill-color', 'rgba(59, 130, 246, 0.1)');
752 } 233 }
753 234
754 async function loadMunicipalStats() { 235 async function loadMunicipalStats() {
@@ -758,10 +239,8 @@ @@ -758,10 +239,8 @@
758 }); 239 });
759 const data = await res.json(); 240 const data = await res.json();
760 document.getElementById('stat-lotes').innerText = data.total_lotes.toLocaleString(); 241 document.getElementById('stat-lotes').innerText = data.total_lotes.toLocaleString();
761 - document.getElementById('stat-morosos').innerText = data.lotes_con_deuda.toLocaleString();  
762 - } catch (err) { console.error("Error stats:", err); } 242 + } catch (e) { console.error(e); }
763 } 243 }
764 </script> 244 </script>
765 </body> 245 </body>
766 -  
767 </html> 246 </html>
768 \ No newline at end of file 247 \ No newline at end of file
test_dash_matches.sql deleted
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 deleted
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 deleted
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 (Regla 23 & 30)  
2 -DROP VIEW IF EXISTS public.vw_lotes_morosidad_505 CASCADE; 1 +-- Punto 1: Actualización de la Vista Maestra de Morosidad con Filtro de Entidad
3 CREATE OR REPLACE VIEW public.vw_lotes_morosidad_505 AS 2 CREATE OR REPLACE VIEW public.vw_lotes_morosidad_505 AS
4 SELECT 3 SELECT
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, '-', '') ; 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';
14 10
15 -- Punto 2: Creación de la Vista de Percentiles Filtrada por Entidad (Nuevo Estándar) 11 -- Punto 2: Creación de la Vista de Percentiles Filtrada por Entidad (Nuevo Estándar)
16 CREATE OR REPLACE VIEW public.vw_percentiles_505 AS 12 CREATE OR REPLACE VIEW public.vw_percentiles_505 AS
urban_record_dump.sql deleted
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 deleted
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 deleted
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