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 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.
6 5  
7 6 Regla 1. El ambiente de desarrollo y compilación se encuentra en el 192.168.1.123.
8 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 86 Para toda vista de unión con el FDW, se debe utilizar la columna `snc_cuenta` (limpia) contra `REPLACE(liq.inm_ctacatastral, '-', '')`.
88 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 88 ```sql
90   -DROP VIEW IF EXISTS public.vw_lotes_morosidad_XXX CASCADE;
91 89 CREATE OR REPLACE VIEW public.vw_lotes_morosidad_XXX AS
92 90 SELECT
93 91 lot.*,
... ... @@ -116,21 +114,13 @@ Almacenamiento en carpeta cronológica dentro de /publico/.
116 114  
117 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 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 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 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 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 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");
19 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 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;
7 6 import org.springframework.web.bind.annotation.*;
8 7 import org.springframework.web.client.RestTemplate;
9 8 import java.util.*;
... ... @@ -512,7 +511,7 @@ public class SncImportController {
512 511 System.out.println(String.format("[%d/%d] PROCESANDO ENTIDAD %s (SNC: %s-%s)", current, total, eid,
513 512 dpto, dist));
514 513 try {
515   - importDistrict(eid, dpto, dist, 0, false, false);
  514 + importDistrict(eid, dpto, dist, false);
516 515 } catch (Exception e) {
517 516 System.err.println("!!! FALLO CRÍTICO EN ENTIDAD " + eid + ": " + e.getMessage());
518 517 }
... ... @@ -527,36 +526,20 @@ public class SncImportController {
527 526 }
528 527  
529 528 @GetMapping("/snc/{entityId}/{dpto}/{dist}")
530   - public ResponseEntity<String> importDistrict(
  529 + public String importDistrict(
531 530 @PathVariable String entityId,
532 531 @PathVariable String dpto,
533 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 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 543 String url = org.springframework.web.util.UriComponentsBuilder
561 544 .fromHttpUrl("https://www.catastro.gov.py/geoserver/ows")
562 545 .queryParam("service", "WFS")
... ... @@ -570,137 +553,90 @@ public class SncImportController {
570 553 .toUriString();
571 554  
572 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 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 567 com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
579 568 List<Object[]> batchArgs = new ArrayList<>();
580 569  
581 570 for (Map<String, Object> feature : features) {
582 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 579 String ccatastral = (String) props.get("ccatastral");
  580 + Integer tc = (Integer) props.get("tipo_cuenta");
586 581 Object padronObj = props.get("padron");
587 582 String padronStr = padronObj != null ? String.valueOf(padronObj) : "";
588 583  
  584 + // REGLA 26: Normalización Universal de Cartografía SNC (Actualizada)
589 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 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 595 snc_cuenta = padronStr;
599 596 }
600 597  
601 598 try {
  599 + String geomJson = mapper.writeValueAsString(shapeObj);
  600 + if (shapeObj == null)
  601 + continue;
  602 +
602 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 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 730 entityId, fdwSchema));
795 731  
796 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 735 "FROM public.e" + entityId + "_lotes_activos lot " +
800 736 "LEFT JOIN " + fdwSchema
... ...
src/main/java/com/sigem/gis/security/SecurityConfig.java
... ... @@ -30,7 +30,6 @@ 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
34 33 .requestMatchers("/login.html", "/", "/mapas/**", "/mapas.html", "/login", "/error", "/landing", "/landing.html", "/widgets", "/widgets.html").permitAll()
35 34 .requestMatchers("/mapas_institucional.html").permitAll()
36 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 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
55   - // OBLIGATORIA
  54 + // 2. Ejecutar comandos DDL en el servidor PostGIS (.123) - RECREACIÓN OBLIGATORIA
56 55 try {
57 56 // Recreación del Servidor y Mapeo
58 57 gisJdbcTemplate.execute("DROP SERVER IF EXISTS " + serverName + " CASCADE");
... ... @@ -75,24 +74,20 @@ public class FdwService {
75 74  
76 75 // Vista de Auditoría (MVT) - REGLA 23
77 76 String viewLotesName = "vw_lotes_morosidad_" + entidadId;
78   - gisJdbcTemplate.execute("DROP VIEW IF EXISTS public." + viewLotesName + " CASCADE");
79 77 gisJdbcTemplate.execute(String.format(
80 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 82 viewLotesName, tableLotes, schemaName));
86 83  
87 84 // Vista PNG FULL (WMS) - REGLA 23
88 85 String viewWmsName = "vw_lotes_wms_" + entidadId;
89   - gisJdbcTemplate.execute("DROP VIEW IF EXISTS public." + viewWmsName + " CASCADE");
90 86 gisJdbcTemplate.execute(String.format(
91 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 91 viewWmsName, tableLotes, schemaName));
97 92  
98 93 // 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">
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 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 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 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 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 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 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 96 </body>
220 97 -</html>
  98 +</html>
221 99 \ No newline at end of file
... ...
src/main/resources/static/mapas.html
1 1 <!DOCTYPE html>
2 2 <html lang="es">
3   -
4 3 <head>
5 4 <meta charset="UTF-8">
6 5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
... ... @@ -14,11 +13,8 @@
14 13 <script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
15 14 <link href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css" rel="stylesheet" />
16 15 <style>
17   - * {
18   - box-sizing: border-box;
19   - }
20   - body,
21   - html {
  16 + * { box-sizing: border-box; }
  17 + body, html {
22 18 height: 100%;
23 19 margin: 0;
24 20 font-family: 'Inter', Arial, sans-serif;
... ... @@ -26,7 +22,6 @@
26 22 color: #fff;
27 23 overflow: hidden;
28 24 }
29   -
30 25 .header {
31 26 height: 60px;
32 27 background: #1e293b;
... ... @@ -34,47 +29,30 @@
34 29 align-items: center;
35 30 justify-content: center;
36 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 33 font-weight: bold;
39 34 position: relative;
40 35 z-index: 1001;
41 36 }
42   -
43 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 40 color: white;
47 41 padding: 8px 16px;
48 42 border-radius: 8px;
49 43 cursor: pointer;
50 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 48 .sidebar {
70   - width: 170px;
  49 + width: 280px;
71 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 52 overflow-y: auto;
74 53 padding: 20px;
75 54 flex-shrink: 0;
76 55 }
77   -
78 56 .menu-title {
79 57 font-size: 11px;
80 58 color: #64748b;
... ... @@ -83,7 +61,6 @@
83 61 margin: 25px 0 10px;
84 62 letter-spacing: 1px;
85 63 }
86   -
87 64 .menu-item {
88 65 padding: 12px 16px;
89 66 border-radius: 10px;
... ... @@ -96,178 +73,14 @@
96 73 margin-bottom: 4px;
97 74 border: 1px solid transparent;
98 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 78 .menu-item.active {
108 79 background: rgba(59, 130, 246, 0.1);
109 80 color: #3b82f6;
110 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 84 .map-legend {
272 85 position: absolute;
273 86 bottom: 30px;
... ... @@ -275,124 +88,56 @@
275 88 background: rgba(15, 23, 42, 0.85);
276 89 padding: 18px;
277 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 92 color: #f1f5f9;
280 93 font-size: 11px;
281 94 z-index: 1000;
282 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 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 101 </style>
313 102 </head>
314   -
315 103 <body>
316 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 106 </div>
319 107 <div class="app-container">
320 108 <div class="sidebar">
321 109 <div id="stats-dashboard">
322 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 114 </div>
328   -
329   -
330 115 </div>
331 116  
332 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 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 128 </div>
361 129  
362 130 <div id="map">
363   - <!-- Leyenda Dinámica -->
364 131 <div class="map-legend" id="legend" style="display: none;">
365 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 134 </div>
370   -
371   -
372   -
373 135 </div>
374 136 </div>
375 137  
376 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 139 const entidad = localStorage.getItem('entidad') || '505';
384 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 142 const map = new maplibregl.Map({
398 143 container: 'map',
... ... @@ -403,7 +148,7 @@
403 148 type: 'raster',
404 149 tiles: ['https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png'],
405 150 tileSize: 256,
406   - attribution: '&copy; CartoDB - SIGEM-REGISTRO MIC/DINAPI 593-7/Julio/2016'
  151 + attribution: '&copy; CartoDB'
407 152 }
408 153 },
409 154 layers: [{
... ... @@ -414,36 +159,26 @@
414 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 166 map.addControl(new maplibregl.NavigationControl({ position: 'bottom-left' }));
425 167  
426   - // --- Carga de Capas Vectoriales ---
427 168 map.on('load', () => {
428 169 initGisSources();
429 170 loadMunicipalStats();
430 171 });
431 172  
432 173 function initGisSources() {
433   - console.log("Cargando fuentes vectoriales (MVT)...");
434   -
435   - // Fuente de Lotes (MVT) - TMS Nativo de GeoServer
436 174 if (!map.getSource('lotes-mvt')) {
437 175 map.addSource('lotes-mvt', {
438 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 178 scheme: 'tms'
443 179 });
444 180 }
445 181  
446   - // Capa de Lotes (2D) - Visibilidad Inicial Mejorada
447 182 if (!map.getLayer('lotes-layer')) {
448 183 map.addLayer({
449 184 id: 'lotes-layer',
... ... @@ -451,116 +186,21 @@
451 186 source: 'lotes-mvt',
452 187 'source-layer': `vw_lotes_morosidad_${entidad}`,
453 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 196 function setHeatmap(type) {
550 197 document.querySelectorAll('.menu-item').forEach(el => el.classList.remove('active'));
551   - const titleEl = document.getElementById('map-title');
552 198 const legendEl = document.getElementById('legend');
553 199 const legendContent = document.getElementById('legend-content');
554 200 legendEl.style.display = 'block';
555 201  
556 202 if (type === 'ultimo-pago') {
557 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 204 legendContent.innerHTML = `
565 205 <strong style="color: #60a5fa;">ÚLTIMO PAGO</strong><br><br>
566 206 <div class="legend-item"><div class="legend-color" style="background: #6b9070;"></div> 2026</div>
... ... @@ -569,186 +209,27 @@
569 209 <div class="legend-item"><div class="legend-color" style="background: #f08060;"></div> 2023</div>
570 210 <div class="legend-item"><div class="legend-color" style="background: #d05660;"></div> 2022</div>
571 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 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 218 } else if (type === 'percentiles') {
587 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 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 228 function resetMap() {
619 229 document.querySelectorAll('.menu-item').forEach(el => el.classList.remove('active'));
620 230 document.getElementById('menu-reset').classList.add('active');
621   - document.getElementById('map-title').textContent = 'Vista Cartográfica General';
622 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 235 async function loadMunicipalStats() {
... ... @@ -758,10 +239,8 @@
758 239 });
759 240 const data = await res.json();
760 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 244 </script>
765 245 </body>
766   -
767 246 </html>
768 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 2 CREATE OR REPLACE VIEW public.vw_lotes_morosidad_505 AS
4 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 11 -- Punto 2: Creación de la Vista de Percentiles Filtrada por Entidad (Nuevo Estándar)
16 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