Commit 34c9927f00ba48166e752d0da2214d69d8e9d54a

Authored by Antigravity AI
1 parent 480b16a1

Ajuste de reglas y vista 505 para control visual

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