diff --git a/src/app/api/eterra/search/route.ts b/src/app/api/eterra/search/route.ts index 63e9736..f5c77f9 100644 --- a/src/app/api/eterra/search/route.ts +++ b/src/app/api/eterra/search/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; import { getSessionCredentials } from "@/modules/parcel-sync/services/session-store"; +import { prisma } from "@/core/storage/prisma"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -30,6 +31,17 @@ globalRef.__eterraWorkspaceCache = workspaceCache; * until we find one whose nomenPk matches the SIRUTA. * Results are cached globally (survives hot-reload). */ +/** + * Resolve eTerra workspace nomenPk for a given SIRUTA. + * + * Strategy (fast path first): + * 1. Check in-memory cache + * 2. Direct nomen lookup: GET /api/adm/nomen/{siruta} → read parentNomenPk + * then GET parent to check if it's a COUNTY → that's the workspace + * 3. Full scan: fetch all counties → their UATs (slow fallback, ~42 calls) + * + * Results are cached globally. + */ async function resolveWorkspace( client: EterraClient, siruta: string, @@ -37,6 +49,40 @@ async function resolveWorkspace( const cached = workspaceCache.get(siruta); if (cached !== undefined) return cached; + // Fast path: direct nomen lookup + try { + const nomen = await client.fetchNomenByPk(siruta); + console.log("[resolveWorkspace] direct nomen lookup for", siruta, "→", JSON.stringify(nomen).slice(0, 500)); + if (nomen) { + // Walk parent chain to find COUNTY + const parentPk = nomen?.parentNomenPk ?? nomen?.parentPk ?? nomen?.parent?.nomenPk; + if (parentPk) { + const parent = await client.fetchNomenByPk(parentPk); + console.log("[resolveWorkspace] parent nomen:", JSON.stringify(parent).slice(0, 500)); + const parentType = String(parent?.nomenType ?? parent?.type ?? "").toUpperCase(); + if (parentType === "COUNTY" || parentType === "JUDET") { + const countyPk = Number(parent?.nomenPk ?? parentPk); + workspaceCache.set(siruta, countyPk); + return countyPk; + } + // Maybe grandparent is the county (UAT → commune → county?) + const grandparentPk = parent?.parentNomenPk ?? parent?.parentPk; + if (grandparentPk) { + const grandparent = await client.fetchNomenByPk(grandparentPk); + const gpType = String(grandparent?.nomenType ?? grandparent?.type ?? "").toUpperCase(); + if (gpType === "COUNTY" || gpType === "JUDET") { + const countyPk = Number(grandparent?.nomenPk ?? grandparentPk); + workspaceCache.set(siruta, countyPk); + return countyPk; + } + } + } + } + } catch { + // Direct lookup failed — continue to full scan + } + + // Slow fallback: iterate all counties and their UATs try { const counties = await client.fetchCounties(); for (const county of counties) { @@ -45,12 +91,17 @@ async function resolveWorkspace( try { const uats = await client.fetchAdminUnitsByCounty(countyPk); for (const uat of uats) { - const uatPk = String(uat?.nomenPk ?? uat?.pk ?? ""); - if (uatPk) { - workspaceCache.set(uatPk, Number(countyPk)); + // Try all possible identifier fields + const candidates = [ + uat?.nomenPk, + uat?.siruta, + uat?.code, + uat?.pk, + ].filter(Boolean).map(String); + for (const c of candidates) { + workspaceCache.set(c, Number(countyPk)); } } - // Check if our SIRUTA is now resolved const resolved = workspaceCache.get(siruta); if (resolved !== undefined) return resolved; } catch { @@ -58,7 +109,7 @@ async function resolveWorkspace( } } } catch { - // fallback: can't fetch counties + // Can't fetch counties } return null; } @@ -187,10 +238,33 @@ export async function POST(req: Request) { const client = await EterraClient.create(username, password); - // Use provided workspacePk — or fall back to resolution + // Use provided workspacePk — or look up from DB — or resolve from eTerra let workspaceId = body.workspacePk ?? null; + console.log( + "[search] siruta:", + siruta, + "body.workspacePk:", + body.workspacePk, + "workspaceId:", + workspaceId, + ); if (!workspaceId || !Number.isFinite(workspaceId)) { + // Try DB lookup first (cheap) + try { + const dbUat = await prisma.gisUat.findUnique({ + where: { siruta }, + select: { workspacePk: true }, + }); + console.log("[search] DB lookup result:", dbUat); + if (dbUat?.workspacePk) workspaceId = dbUat.workspacePk; + } catch (e) { + console.log("[search] DB lookup error:", e); + } + } + if (!workspaceId || !Number.isFinite(workspaceId)) { + console.log("[search] falling back to resolveWorkspace..."); workspaceId = await resolveWorkspace(client, siruta); + console.log("[search] resolveWorkspace result:", workspaceId); } if (!workspaceId) { return NextResponse.json( diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index e3c4c70..00585a2 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -328,21 +328,17 @@ export function ParcelSyncModule() { // Load UATs from local DB (fast — no eTerra needed) fetch("/api/eterra/uats") .then((res) => res.json()) - .then( - (data: { - uats?: UatEntry[]; - }) => { - if (data.uats && data.uats.length > 0) { - setUatData(data.uats); - } else { - // DB empty — fall back to static uat.json (no county/workspace) - fetch("/uat.json") - .then((res) => res.json()) - .then((fallback: UatEntry[]) => setUatData(fallback)) - .catch(() => {}); - } - }, - ) + .then((data: { uats?: UatEntry[] }) => { + if (data.uats && data.uats.length > 0) { + setUatData(data.uats); + } else { + // DB empty — fall back to static uat.json (no county/workspace) + fetch("/uat.json") + .then((res) => res.json()) + .then((fallback: UatEntry[]) => setUatData(fallback)) + .catch(() => {}); + } + }) .catch(() => { // API failed — fall back to static uat.json fetch("/uat.json") @@ -372,23 +368,19 @@ export function ParcelSyncModule() { // POST triggers sync check — only does full fetch if data changed fetch("/api/eterra/uats", { method: "POST" }) .then((res) => res.json()) - .then( - (data: { synced?: boolean }) => { - if (data.synced) { - // Data changed — reload from DB - fetch("/api/eterra/uats") - .then((res) => res.json()) - .then( - (fresh: { uats?: UatEntry[] }) => { - if (fresh.uats && fresh.uats.length > 0) { - setUatData(fresh.uats); - } - }, - ) - .catch(() => {}); - } - }, - ) + .then((data: { synced?: boolean }) => { + if (data.synced) { + // Data changed — reload from DB + fetch("/api/eterra/uats") + .then((res) => res.json()) + .then((fresh: { uats?: UatEntry[] }) => { + if (fresh.uats && fresh.uats.length > 0) { + setUatData(fresh.uats); + } + }) + .catch(() => {}); + } + }) .catch(() => {}); }, [session.connected]); diff --git a/src/modules/parcel-sync/services/eterra-client.ts b/src/modules/parcel-sync/services/eterra-client.ts index 2f5f586..31d0a2e 100644 --- a/src/modules/parcel-sync/services/eterra-client.ts +++ b/src/modules/parcel-sync/services/eterra-client.ts @@ -560,6 +560,20 @@ export class EterraClient { return this.getRawJson(url); } + /** + * Fetch a single nomenclature entry by nomenPk. + * Returns { nomenPk, name, parentNomenPk, nomenType, ... } or null + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async fetchNomenByPk(nomenPk: string | number): Promise { + const url = `${BASE_URL}/api/adm/nomen/${nomenPk}`; + try { + return await this.getRawJson(url); + } catch { + return null; + } + } + /** * Fetch administrative units (UATs) under a county workspace. * Returns array of { nomenPk, name, parentNomenPk, ... }