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:
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user