diff --git a/docker-compose.yml b/docker-compose.yml
index 66bee13..881611b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -91,3 +91,12 @@ services:
timeout: 5s
retries: 3
start_period: 10s
+
+ martin:
+ image: ghcr.io/maplibre/martin:v0.15.0
+ container_name: martin
+ restart: unless-stopped
+ ports:
+ - "3010:3000"
+ environment:
+ - DATABASE_URL=postgresql://architools_user:stictMyFon34!_gonY@10.10.10.166:5432/architools_db
diff --git a/package-lock.json b/package-lock.json
index bc477b5..003aa10 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"jspdf": "^4.2.0",
"jszip": "^3.10.1",
"lucide-react": "^0.564.0",
+ "maplibre-gl": "^5.21.0",
"minio": "^8.0.6",
"next": "16.1.6",
"next-auth": "^4.24.13",
@@ -1641,6 +1642,111 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@mapbox/jsonlint-lines-primitives": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
+ "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@mapbox/point-geometry": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz",
+ "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==",
+ "license": "ISC"
+ },
+ "node_modules/@mapbox/tiny-sdf": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz",
+ "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/@mapbox/unitbezier": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
+ "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/@mapbox/vector-tile": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz",
+ "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@mapbox/point-geometry": "~1.1.0",
+ "@types/geojson": "^7946.0.16",
+ "pbf": "^4.0.1"
+ }
+ },
+ "node_modules/@mapbox/whoots-js": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
+ "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@maplibre/geojson-vt": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.0.4.tgz",
+ "integrity": "sha512-HYv3POhMRCdhP3UPPATM/hfcy6/WuVIf5FKboH8u/ZuFMTnAIcSVlq5nfOqroLokd925w2QtE7YwquFOIacwVQ==",
+ "license": "ISC",
+ "dependencies": {
+ "kdbush": "^4.0.2"
+ }
+ },
+ "node_modules/@maplibre/maplibre-gl-style-spec": {
+ "version": "24.7.0",
+ "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.7.0.tgz",
+ "integrity": "sha512-Ed7rcKYU5iELfablg9Mj+TVCsXsPBgdMyXPRAxb2v7oWg9YJnpQdZ5msDs1LESu/mtXy3Z48Vdppv2t/x5kAhw==",
+ "license": "ISC",
+ "dependencies": {
+ "@mapbox/jsonlint-lines-primitives": "~2.0.2",
+ "@mapbox/unitbezier": "^0.0.1",
+ "json-stringify-pretty-compact": "^4.0.0",
+ "minimist": "^1.2.8",
+ "quickselect": "^3.0.0",
+ "rw": "^1.3.3",
+ "tinyqueue": "^3.0.0"
+ },
+ "bin": {
+ "gl-style-format": "dist/gl-style-format.mjs",
+ "gl-style-migrate": "dist/gl-style-migrate.mjs",
+ "gl-style-validate": "dist/gl-style-validate.mjs"
+ }
+ },
+ "node_modules/@maplibre/mlt": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.8.tgz",
+ "integrity": "sha512-8vtfYGidr1rNkv5IwIoU2lfe3Oy+Wa8HluzQYcQi9cveU9K3pweAal/poQj4GJ0K/EW4bTQp2wVAs09g2yDRZg==",
+ "license": "(MIT OR Apache-2.0)",
+ "dependencies": {
+ "@mapbox/point-geometry": "^1.1.0"
+ }
+ },
+ "node_modules/@maplibre/vt-pbf": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.0.tgz",
+ "integrity": "sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@mapbox/point-geometry": "^1.1.0",
+ "@mapbox/vector-tile": "^2.0.4",
+ "@maplibre/geojson-vt": "^5.0.4",
+ "@types/geojson": "^7946.0.16",
+ "@types/supercluster": "^7.1.3",
+ "pbf": "^4.0.1",
+ "supercluster": "^8.0.1"
+ }
+ },
+ "node_modules/@maplibre/vt-pbf/node_modules/@maplibre/geojson-vt": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz",
+ "integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==",
+ "license": "ISC"
+ },
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.26.0",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
@@ -4015,6 +4121,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -4118,6 +4230,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/supercluster": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
+ "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@types/tough-cookie": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
@@ -6293,6 +6414,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/earcut": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz",
+ "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==",
+ "license": "ISC"
+ },
"node_modules/eciesjs": {
"version": "0.4.17",
"resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.17.tgz",
@@ -7794,6 +7921,12 @@
"giget": "dist/cli.mjs"
}
},
+ "node_modules/gl-matrix": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz",
+ "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
+ "license": "MIT"
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -8938,6 +9071,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/json-stringify-pretty-compact": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
+ "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
+ "license": "MIT"
+ },
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -9009,6 +9148,12 @@
"setimmediate": "^1.0.5"
}
},
+ "node_modules/kdbush": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
+ "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
+ "license": "ISC"
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -9454,6 +9599,40 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/maplibre-gl": {
+ "version": "5.21.0",
+ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.21.0.tgz",
+ "integrity": "sha512-n0v4J/Ge0EG8ix/z3TY3ragtJYMqzbtSnj1riOC0OwQbzwp0lUF2maS1ve1z8HhitQCKtZZiZJhb8to36aMMfQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@mapbox/jsonlint-lines-primitives": "^2.0.2",
+ "@mapbox/point-geometry": "^1.1.0",
+ "@mapbox/tiny-sdf": "^2.0.7",
+ "@mapbox/unitbezier": "^0.0.1",
+ "@mapbox/vector-tile": "^2.0.4",
+ "@mapbox/whoots-js": "^3.1.0",
+ "@maplibre/geojson-vt": "^6.0.4",
+ "@maplibre/maplibre-gl-style-spec": "^24.7.0",
+ "@maplibre/mlt": "^1.1.8",
+ "@maplibre/vt-pbf": "^4.3.0",
+ "@types/geojson": "^7946.0.16",
+ "earcut": "^3.0.2",
+ "gl-matrix": "^3.4.4",
+ "kdbush": "^4.0.2",
+ "murmurhash-js": "^1.0.0",
+ "pbf": "^4.0.1",
+ "potpack": "^2.1.0",
+ "quickselect": "^3.0.0",
+ "tinyqueue": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.14.0",
+ "npm": ">=8.1.0"
+ },
+ "funding": {
+ "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -9596,7 +9775,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -9723,6 +9901,12 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/murmurhash-js": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
+ "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
+ "license": "MIT"
+ },
"node_modules/mute-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
@@ -10555,6 +10739,18 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/pbf": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
+ "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "resolve-protobuf-schema": "^2.1.0"
+ },
+ "bin": {
+ "pbf": "bin/pbf"
+ }
+ },
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
@@ -10671,6 +10867,12 @@
"node": ">=4"
}
},
+ "node_modules/potpack": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz",
+ "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==",
+ "license": "ISC"
+ },
"node_modules/powershell-utils": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz",
@@ -10821,6 +11023,12 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/protocol-buffers-schema": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
+ "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
+ "license": "MIT"
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -11085,6 +11293,12 @@
],
"license": "MIT"
},
+ "node_modules/quickselect": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
+ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
+ "license": "ISC"
+ },
"node_modules/radix-ui": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz",
@@ -11476,6 +11690,15 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
+ "node_modules/resolve-protobuf-schema": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
+ "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "protocol-buffers-schema": "^3.3.1"
+ }
+ },
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
@@ -11586,6 +11809,12 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -12444,6 +12673,15 @@
}
}
},
+ "node_modules/supercluster": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
+ "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
+ "license": "ISC",
+ "dependencies": {
+ "kdbush": "^4.0.2"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -12667,6 +12905,12 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/tinyqueue": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
+ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
+ "license": "ISC"
+ },
"node_modules/tldts": {
"version": "7.0.23",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz",
diff --git a/package.json b/package.json
index 28f6ab1..8e68656 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"jspdf": "^4.2.0",
"jszip": "^3.10.1",
"lucide-react": "^0.564.0",
+ "maplibre-gl": "^5.21.0",
"minio": "^8.0.6",
"next": "16.1.6",
"next-auth": "^4.24.13",
diff --git a/prisma/gisuat-postgis-setup.sql b/prisma/gisuat-postgis-setup.sql
new file mode 100644
index 0000000..32b26bc
--- /dev/null
+++ b/prisma/gisuat-postgis-setup.sql
@@ -0,0 +1,177 @@
+-- =============================================================================
+-- PostGIS native geometry setup for GisUat (UAT boundaries)
+-- Run once manually: PGPASSWORD='...' psql -h 10.10.10.166 -p 5432 \
+-- -U architools_user -d architools_db -f prisma/gisuat-postgis-setup.sql
+--
+-- Idempotent — safe to re-run.
+--
+-- What this does:
+-- 1. Ensures PostGIS extension
+-- 2. Adds native geometry column (geom) if missing
+-- 3. Creates function to convert Esri ring JSON -> PostGIS geometry
+-- 4. Creates trigger to auto-convert on INSERT/UPDATE
+-- 5. Backfills existing rows
+-- 6. Creates GiST spatial index
+-- 7. Creates Martin/QGIS-friendly view 'gis_uats'
+--
+-- After running both SQL scripts (postgis-setup.sql + this file), Martin
+-- will auto-discover these views (any table/view with a 'geom' geometry column):
+-- - gis_features (master: all GisFeature rows with geometry)
+-- - gis_terenuri (parcels from GisFeature)
+-- - gis_cladiri (buildings from GisFeature)
+-- - gis_documentatii (expertize/zone/receptii from GisFeature)
+-- - gis_administrativ (limite UAT/intravilan/arii speciale from GisFeature)
+-- - gis_uats (UAT boundaries from GisUat) <-- this script
+--
+-- All geometries are in EPSG:3844 (Stereo70).
+-- =============================================================================
+
+-- 1. Ensure PostGIS extension
+CREATE EXTENSION IF NOT EXISTS postgis;
+
+-- 2. Add native geometry column (idempotent)
+DO $$
+BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM information_schema.columns
+ WHERE table_name = 'GisUat' AND column_name = 'geom'
+ ) THEN
+ ALTER TABLE "GisUat" ADD COLUMN geom geometry(Geometry, 3844);
+ END IF;
+END $$;
+
+-- 3. Function: convert Esri ring JSON { rings: number[][][] } -> PostGIS geometry
+-- Esri rings format: each ring is an array of [x, y] coordinate pairs.
+-- First ring = exterior, subsequent rings = holes.
+-- Multiple outer rings (non-holes) would need MultiPolygon, but UAT boundaries
+-- from eTerra typically have a single polygon with possible holes.
+--
+-- Strategy: build WKT POLYGON/MULTIPOLYGON from the rings array, then
+-- use ST_GeomFromText with SRID 3844.
+CREATE OR REPLACE FUNCTION gis_uat_esri_to_geom(geom_json jsonb)
+RETURNS geometry AS $$
+DECLARE
+ rings jsonb;
+ ring jsonb;
+ coord jsonb;
+ ring_count int;
+ coord_count int;
+ i int;
+ j int;
+ wkt_ring text;
+ wkt text;
+ first_x double precision;
+ first_y double precision;
+ last_x double precision;
+ last_y double precision;
+BEGIN
+ -- Extract the rings array from the JSON
+ rings := geom_json -> 'rings';
+
+ IF rings IS NULL OR jsonb_array_length(rings) = 0 THEN
+ RETURN NULL;
+ END IF;
+
+ ring_count := jsonb_array_length(rings);
+
+ -- Build WKT POLYGON with all rings (first = exterior, rest = holes)
+ wkt := 'POLYGON(';
+
+ FOR i IN 0 .. ring_count - 1 LOOP
+ ring := rings -> i;
+ coord_count := jsonb_array_length(ring);
+
+ IF coord_count < 3 THEN
+ CONTINUE; -- skip degenerate rings
+ END IF;
+
+ IF i > 0 THEN
+ wkt := wkt || ', ';
+ END IF;
+
+ wkt_ring := '(';
+
+ FOR j IN 0 .. coord_count - 1 LOOP
+ coord := ring -> j;
+ IF j > 0 THEN
+ wkt_ring := wkt_ring || ', ';
+ END IF;
+ wkt_ring := wkt_ring || (coord ->> 0) || ' ' || (coord ->> 1);
+
+ -- Track first and last coordinates to check ring closure
+ IF j = 0 THEN
+ first_x := (coord ->> 0)::double precision;
+ first_y := (coord ->> 1)::double precision;
+ END IF;
+ IF j = coord_count - 1 THEN
+ last_x := (coord ->> 0)::double precision;
+ last_y := (coord ->> 1)::double precision;
+ END IF;
+ END LOOP;
+
+ -- Close the ring if not already closed
+ IF first_x != last_x OR first_y != last_y THEN
+ wkt_ring := wkt_ring || ', ' || first_x::text || ' ' || first_y::text;
+ END IF;
+
+ wkt_ring := wkt_ring || ')';
+ wkt := wkt || wkt_ring;
+ END LOOP;
+
+ wkt := wkt || ')';
+
+ RETURN ST_GeomFromText(wkt, 3844);
+END;
+$$ LANGUAGE plpgsql IMMUTABLE;
+
+-- 4. Trigger function: auto-convert Esri JSON -> native PostGIS on INSERT/UPDATE
+CREATE OR REPLACE FUNCTION gis_uat_sync_geom()
+RETURNS TRIGGER AS $$
+BEGIN
+ IF NEW.geometry IS NOT NULL THEN
+ BEGIN
+ NEW.geom := gis_uat_esri_to_geom(NEW.geometry::jsonb);
+ EXCEPTION WHEN OTHERS THEN
+ -- Invalid geometry JSON -> leave geom NULL rather than fail the write
+ NEW.geom := NULL;
+ END;
+ ELSE
+ NEW.geom := NULL;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+
+-- 5. Attach trigger (drop + recreate for idempotency)
+DROP TRIGGER IF EXISTS trg_gis_uat_sync_geom ON "GisUat";
+CREATE TRIGGER trg_gis_uat_sync_geom
+ BEFORE INSERT OR UPDATE OF geometry ON "GisUat"
+ FOR EACH ROW
+ EXECUTE FUNCTION gis_uat_sync_geom();
+
+-- 6. Backfill: convert existing Esri JSON geometries to native PostGIS
+UPDATE "GisUat"
+SET geom = gis_uat_esri_to_geom(geometry::jsonb)
+WHERE geometry IS NOT NULL AND geom IS NULL;
+
+-- 7. GiST spatial index for fast spatial queries
+CREATE INDEX IF NOT EXISTS gis_uat_geom_idx
+ ON "GisUat" USING GIST (geom);
+
+-- 8. Martin/QGIS-friendly view
+CREATE OR REPLACE VIEW gis_uats AS
+SELECT
+ siruta,
+ name,
+ county,
+ "workspacePk" AS workspace_pk,
+ "areaValue" AS area_value,
+ geom
+FROM "GisUat"
+WHERE geom IS NOT NULL;
+
+-- =============================================================================
+-- Done! Martin will auto-discover gis_uats at http://localhost:3010/gis_uats
+-- QGIS: PostgreSQL -> 10.10.10.166:5432 / architools_db -> gis_uats view
+-- SRID: 3844 (Stereo70)
+-- =============================================================================
diff --git a/src/app/(modules)/geoportal/page.tsx b/src/app/(modules)/geoportal/page.tsx
new file mode 100644
index 0000000..ec67fde
--- /dev/null
+++ b/src/app/(modules)/geoportal/page.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { FeatureGate } from "@/core/feature-flags";
+import { useI18n } from "@/core/i18n";
+import { GeoportalModule } from "@/modules/geoportal";
+
+export default function GeoportalPage() {
+ const { t } = useI18n();
+
+ return (
+
+ {t("geoportal.description")} +
+Modulul Geoportal este dezactivat.
+Se incarca harta...
++ Harta interactiva cu parcele cadastrale, cladiri si limite UAT +
++ {group.description} +
+Fara atribute
"; + return `Se incarca harta...
+Se incarca harta...
+