From 86c39473a503c334f931ad0b010db7e7cbf28af4 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Sun, 22 Mar 2026 20:46:13 +0200 Subject: [PATCH] feat(parcel-sync): show county in UAT search dropdown via eTerra data PATCH /api/eterra/uats fetches counties from eTerra nomenclature and LIMITE_UAT layer, then batch-updates GisUat records with county name and workspacePk. Auto-triggers on first eTerra connection when county data is missing. Helps distinguish same-name UATs in different counties. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/eterra/uats/route.ts | 183 ++++++++++++++++++ .../components/parcel-sync-module.tsx | 32 ++- src/modules/parcel-sync/types.ts | 1 + 3 files changed, 214 insertions(+), 2 deletions(-) diff --git a/src/app/api/eterra/uats/route.ts b/src/app/api/eterra/uats/route.ts index c5f6c7e..d78744f 100644 --- a/src/app/api/eterra/uats/route.ts +++ b/src/app/api/eterra/uats/route.ts @@ -2,6 +2,9 @@ import { NextResponse } from "next/server"; import { prisma } from "@/core/storage/prisma"; import { readFile } from "fs/promises"; import { join } from "path"; +import { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; +import { findLayerById } from "@/modules/parcel-sync/services/eterra-layers"; +import { getSessionCredentials } from "@/modules/parcel-sync/services/session-store"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -151,3 +154,183 @@ export async function POST() { ); } } + +/* ------------------------------------------------------------------ */ +/* PATCH /api/eterra/uats */ +/* */ +/* Populate county names from eTerra. */ +/* 1. fetchCounties() → build workspacePk → county name map */ +/* 2. Query LIMITE_UAT layer (all features, no geometry) to get */ +/* SIRUTA → WORKSPACE_ID mapping for every UAT */ +/* 3. Batch-update GisUat.county + GisUat.workspacePk */ +/* */ +/* Requires active eTerra session. */ +/* ------------------------------------------------------------------ */ + +export async function PATCH() { + try { + // 1. Get eTerra credentials from session + const session = getSessionCredentials(); + const username = String( + session?.username || process.env.ETERRA_USERNAME || "", + ).trim(); + const password = String( + session?.password || process.env.ETERRA_PASSWORD || "", + ).trim(); + + if (!username || !password) { + return NextResponse.json( + { error: "Conectează-te la eTerra mai întâi." }, + { status: 401 }, + ); + } + + const client = await EterraClient.create(username, password); + + // 2. Fetch all counties from eTerra nomenclature → workspacePk → county name + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const counties: any[] = await client.fetchCounties(); + const countyMap = new Map(); + for (const c of counties) { + const pk = Number(c?.nomenPk ?? 0); + const name = String(c?.name ?? "").trim(); + if (pk > 0 && name) { + // Title-case: "CLUJ" → "Cluj", "SATU MARE" → "Satu Mare" + const titleCase = name + .toLowerCase() + .replace(/(?:^|\s)\S/g, (ch) => ch.toUpperCase()); + countyMap.set(pk, titleCase); + } + } + + if (countyMap.size === 0) { + return NextResponse.json( + { error: "Nu s-au putut obține județele din eTerra." }, + { status: 502 }, + ); + } + + console.log(`[uats-patch] Fetched ${countyMap.size} counties from eTerra`); + + // 3. Query LIMITE_UAT layer for all features (no geometry) + // to get SIRUTA + WORKSPACE_ID per UAT + const limiteUat = findLayerById("LIMITE_UAT"); + if (!limiteUat) { + return NextResponse.json( + { error: "Layer LIMITE_UAT nu este configurat." }, + { status: 500 }, + ); + } + + // Discover field names + const fields = await client.getLayerFieldNames(limiteUat); + const upperFields = fields.map((f) => f.toUpperCase()); + + // Find the SIRUTA/admin field + const adminFieldCandidates = [ + "ADMIN_UNIT_ID", + "SIRUTA", + "UAT_ID", + "SIRUTA_UAT", + "UAT_SIRUTA", + ]; + let adminField: string | null = null; + for (const key of adminFieldCandidates) { + const idx = upperFields.indexOf(key); + if (idx >= 0) { + adminField = fields[idx] ?? null; + break; + } + } + + // Find the WORKSPACE_ID field + const wsFieldCandidates = ["WORKSPACE_ID", "COUNTY_ID", "JUDET_ID"]; + let wsField: string | null = null; + for (const key of wsFieldCandidates) { + const idx = upperFields.indexOf(key); + if (idx >= 0) { + wsField = fields[idx] ?? null; + break; + } + } + + if (!adminField) { + return NextResponse.json( + { + error: `LIMITE_UAT nu are câmp SIRUTA. Câmpuri disponibile: ${fields.join(", ")}`, + }, + { status: 500 }, + ); + } + + // Fetch all features — attributes only, no geometry + const outFields = [adminField, wsField, "NAME", "NUME", "UAT_NAME"] + .filter(Boolean) + .join(","); + + const features = await client.fetchAllLayerByWhere(limiteUat, "1=1", { + outFields, + returnGeometry: false, + pageSize: 1000, + }); + + console.log( + `[uats-patch] Fetched ${features.length} UAT features from LIMITE_UAT`, + ); + + // 4. Build SIRUTA → { county, workspacePk } map + const sirutaCountyMap = new Map< + string, + { county: string; workspacePk: number } + >(); + + for (const f of features) { + const attrs = f.attributes; + const sirutaVal = String(attrs[adminField] ?? "") + .trim() + .replace(/\.0$/, ""); + const wsPk = Number(attrs[wsField ?? ""] ?? 0); + + if (!sirutaVal || !Number.isFinite(wsPk) || wsPk <= 0) continue; + + const countyName = countyMap.get(wsPk); + if (!countyName) continue; + + sirutaCountyMap.set(sirutaVal, { county: countyName, workspacePk: wsPk }); + } + + console.log( + `[uats-patch] Mapped ${sirutaCountyMap.size} SIRUTAs to counties`, + ); + + // 5. Batch-update GisUat records + let updated = 0; + const CHUNK_SIZE = 100; + const entries = Array.from(sirutaCountyMap.entries()); + + for (let i = 0; i < entries.length; i += CHUNK_SIZE) { + const chunk = entries.slice(i, i + CHUNK_SIZE); + const ops = chunk.map(([s, data]) => + prisma.gisUat.updateMany({ + where: { siruta: s }, + data: { county: data.county, workspacePk: data.workspacePk }, + }), + ); + const results = await prisma.$transaction(ops); + for (const r of results) updated += r.count; + } + + console.log(`[uats-patch] Updated ${updated} UAT records with county`); + + return NextResponse.json({ + updated, + totalCounties: countyMap.size, + totalMapped: sirutaCountyMap.size, + totalFeatures: features.length, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Eroare server"; + console.error("[uats-patch] Error:", message); + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index cfe1a73..6cf54f1 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -553,10 +553,38 @@ export function ParcelSyncModule() { }, [fetchDbSummary]); /* ════════════════════════════════════════════════════════════ */ - /* (Sync effect removed — POST seeds from uat.json, no */ - /* eTerra nomenclature needed. Workspace resolved lazily.) */ + /* Auto-refresh county data when eTerra is connected */ /* ════════════════════════════════════════════════════════════ */ + const countyRefreshAttempted = useRef(false); + + useEffect(() => { + if (!session.connected || countyRefreshAttempted.current) return; + if (uatData.length === 0) return; + + // Check if most UATs are missing county data + const withCounty = uatData.filter((u) => u.county).length; + if (withCounty > uatData.length * 0.5) return; // >50% already have county + + countyRefreshAttempted.current = true; + console.log("[uats] Auto-refreshing county data from eTerra…"); + + fetch("/api/eterra/uats", { method: "PATCH" }) + .then((res) => res.json()) + .then((result: { updated?: number; error?: string }) => { + if (result.updated && result.updated > 0) { + // Reload UAT data with fresh county info + fetch("/api/eterra/uats") + .then((res) => res.json()) + .then((data: { uats?: UatEntry[] }) => { + if (data.uats && data.uats.length > 0) setUatData(data.uats); + }) + .catch(() => {}); + } + }) + .catch(() => {}); + }, [session.connected, uatData]); + /* ════════════════════════════════════════════════════════════ */ /* UAT autocomplete filter */ /* ════════════════════════════════════════════════════════════ */ diff --git a/src/modules/parcel-sync/types.ts b/src/modules/parcel-sync/types.ts index 092c49b..96a907a 100644 --- a/src/modules/parcel-sync/types.ts +++ b/src/modules/parcel-sync/types.ts @@ -6,6 +6,7 @@ export type UatEntry = { siruta: string; name: string; county?: string; + workspacePk?: number; }; export type LayerSyncStatus = {