feat(parcel-sync): live eTerra search by cadastral number

- Add /api/eterra/search  queries eTerra ArcGIS REST API directly
  by NATIONAL_CADASTRAL_REFERENCE, NATIONAL_CADNR, or INSPIRE_ID
  across TERENURI_ACTIVE + CLADIRI_ACTIVE layers
- Search tab now queries eTerra live (not local DB) with 600ms debounce
- Requires session connected + UAT selected to search
- Updated placeholder and empty state messages in Romanian
This commit is contained in:
AI Assistant
2026-03-06 19:18:18 +02:00
parent bd90c4e30f
commit c98ce81cb7
2 changed files with 249 additions and 15 deletions
+202
View File
@@ -0,0 +1,202 @@
import { NextResponse } from "next/server";
import { EterraClient } from "@/modules/parcel-sync/services/eterra-client";
import { getSessionCredentials } from "@/modules/parcel-sync/services/session-store";
import { LAYER_CATALOG } from "@/modules/parcel-sync/services/eterra-layers";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
type Body = {
siruta?: string;
search?: string;
layerId?: string;
username?: string;
password?: string;
};
/**
* Live search eTerra by cadastral number / INSPIRE ID.
* Queries the remote eTerra ArcGIS REST API directly (not local DB).
*/
export async function POST(req: Request) {
try {
const body = (await req.json()) as Body;
const siruta = String(body.siruta ?? "").trim();
const search = (body.search ?? "").trim();
const layerId = (body.layerId ?? "").trim();
if (!siruta || !/^\d+$/.test(siruta)) {
return NextResponse.json(
{ error: "SIRUTA obligatoriu" },
{ status: 400 },
);
}
if (!search) {
return NextResponse.json(
{ error: "Termen de căutare obligatoriu" },
{ status: 400 },
);
}
// Credential chain: body > session > env
const session = getSessionCredentials();
const username = String(
body.username || session?.username || process.env.ETERRA_USERNAME || "",
).trim();
const password = String(
body.password || 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);
// Decide which layers to search
const searchLayers = layerId
? LAYER_CATALOG.filter((l) => l.id === layerId)
: LAYER_CATALOG.filter((l) =>
["TERENURI_ACTIVE", "CLADIRI_ACTIVE"].includes(l.id),
);
if (searchLayers.length === 0) {
return NextResponse.json({ features: [], total: 0 });
}
// Build the search WHERE — exact or LIKE depending on input
const isNumericOnly = /^\d+$/.test(search);
const escapedSearch = search.replace(/'/g, "''");
type FoundFeature = {
id: string;
layerId: string;
siruta: string;
objectId: number;
inspireId?: string;
cadastralRef?: string;
areaValue?: number;
isActive: boolean;
attributes: Record<string, unknown>;
createdAt: string;
updatedAt: string;
};
const allResults: FoundFeature[] = [];
for (const layer of searchLayers) {
try {
// Get available fields for this layer
const fields = await client.getLayerFieldNames(layer);
const upperFields = fields.map((f) => f.toUpperCase());
// Find admin field for siruta filter
const adminFields = [
"ADMIN_UNIT_ID",
"SIRUTA",
"UAT_ID",
"SIRUTA_UAT",
"UAT_SIRUTA",
];
const adminField = adminFields.find((a) =>
upperFields.includes(a),
);
if (!adminField) continue;
// Get actual casing from layer fields
const adminFieldActual =
fields[upperFields.indexOf(adminField)] ?? adminField;
// Build search conditions depending on available fields
const conditions: string[] = [];
const hasCadRef = upperFields.includes(
"NATIONAL_CADASTRAL_REFERENCE",
);
const hasInspire = upperFields.includes("INSPIRE_ID");
const hasCadNr = upperFields.includes("NATIONAL_CADNR");
if (hasCadRef) {
const cadRefField =
fields[upperFields.indexOf("NATIONAL_CADASTRAL_REFERENCE")]!;
if (isNumericOnly) {
// Exact match for numeric cadastral numbers
conditions.push(`${cadRefField}='${escapedSearch}'`);
} else {
conditions.push(
`${cadRefField} LIKE '%${escapedSearch}%'`,
);
}
}
if (hasCadNr) {
const cadNrField =
fields[upperFields.indexOf("NATIONAL_CADNR")]!;
if (isNumericOnly) {
conditions.push(`${cadNrField}='${escapedSearch}'`);
} else {
conditions.push(`${cadNrField} LIKE '%${escapedSearch}%'`);
}
}
if (hasInspire) {
const inspireField =
fields[upperFields.indexOf("INSPIRE_ID")]!;
conditions.push(
`${inspireField} LIKE '%${escapedSearch}%'`,
);
}
if (conditions.length === 0) continue;
const searchWhere = `${adminFieldActual}=${siruta} AND (${conditions.join(" OR ")})`;
const features = await client.listLayerByWhere(layer, searchWhere, {
limit: 50,
outFields: "*",
});
const now = new Date().toISOString();
for (const f of features) {
const attrs = f.attributes;
const objId =
typeof attrs.OBJECTID === "number"
? attrs.OBJECTID
: Number(attrs.OBJECTID ?? 0);
allResults.push({
id: `live-${layer.id}-${objId}`,
layerId: layer.id,
siruta,
objectId: objId,
inspireId: (attrs.INSPIRE_ID as string | undefined) ?? undefined,
cadastralRef:
(attrs.NATIONAL_CADASTRAL_REFERENCE as string | undefined) ??
(attrs.NATIONAL_CADNR as string | undefined) ??
undefined,
areaValue:
typeof attrs.AREA_VALUE === "number"
? attrs.AREA_VALUE
: undefined,
isActive: attrs.IS_ACTIVE !== 0,
attributes: attrs as Record<string, unknown>,
createdAt: now,
updatedAt: now,
});
}
} catch {
// Skip layer on error and try next
continue;
}
}
return NextResponse.json({
features: allResults,
total: allResults.length,
source: "eterra-live",
});
} catch (error) {
const message = error instanceof Error ? error.message : "Eroare server";
return NextResponse.json({ error: message }, { status: 500 });
}
}