Commit 34c9927f00ba48166e752d0da2214d69d8e9d54a
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
check_cols_505.sql
0 → 100644
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
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
count_505.sql
0 → 100644
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
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
find_oviedo_real_codes.sql
0 → 100644
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
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
get_bounds_master.sql
0 → 100644
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
peek_fdw_keys.sql
0 → 100644
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
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: '© CartoDB' | |
| 406 | + attribution: '© 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
truncate_505.sql
0 → 100644
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
verify_oviedo_master.sql
0 → 100644
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; | ... | ... |