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) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-22 20:46:13 +02:00
parent 2a25e4b160
commit 86c39473a5
3 changed files with 214 additions and 2 deletions
+183
View File
@@ -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<number, string>();
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 });
}
}
@@ -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 */
/* ════════════════════════════════════════════════════════════ */
+1
View File
@@ -6,6 +6,7 @@ export type UatEntry = {
siruta: string;
name: string;
county?: string;
workspacePk?: number;
};
export type LayerSyncStatus = {