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:
@@ -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 */
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
|
||||
@@ -6,6 +6,7 @@ export type UatEntry = {
|
||||
siruta: string;
|
||||
name: string;
|
||||
county?: string;
|
||||
workspacePk?: number;
|
||||
};
|
||||
|
||||
export type LayerSyncStatus = {
|
||||
|
||||
Reference in New Issue
Block a user