feat: add parcel-sync module (eTerra ANCPI integration with PostGIS)

- 31 eTerra layer catalog (terenuri, cladiri, documentatii, administrativ)
- Incremental sync engine (OBJECTID comparison, only downloads new features)
- PostGIS-ready Prisma schema (GisFeature, GisSyncRun, GisUat models)
- 7 API routes (/api/eterra/login, count, sync, features, layers/summary, progress, sync-status)
- Full UI with 3 tabs (Sincronizare, Parcele, Istoric)
- Env var auth (ETERRA_USERNAME / ETERRA_PASSWORD)
- Real-time sync progress tracking with polling
This commit is contained in:
AI Assistant
2026-03-06 00:36:29 +02:00
parent 51dbfcb2bd
commit 7cdea66fa2
25 changed files with 3097 additions and 12 deletions
+59
View File
@@ -0,0 +1,59 @@
import { NextResponse } from "next/server";
import { EterraClient } from "@/modules/parcel-sync/services/eterra-client";
import { findLayerById } from "@/modules/parcel-sync/services/eterra-layers";
import { fetchUatGeometry } from "@/modules/parcel-sync/services/uat-geometry";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
type Body = {
username?: string;
password?: string;
siruta?: string | number;
layerId?: string;
};
export async function POST(req: Request) {
try {
const body = (await req.json()) as Body;
const username = (
body.username ??
process.env.ETERRA_USERNAME ??
""
).trim();
const password = (
body.password ??
process.env.ETERRA_PASSWORD ??
""
).trim();
const siruta = String(body.siruta ?? "").trim();
if (!username || !password)
return NextResponse.json({ error: "Credențiale lipsă" }, { status: 400 });
if (!/^\d+$/.test(siruta))
return NextResponse.json({ error: "SIRUTA invalid" }, { status: 400 });
const layerId = body.layerId ? String(body.layerId) : undefined;
const layer = layerId ? findLayerById(layerId) : undefined;
if (layerId && !layer)
return NextResponse.json({ error: "Layer necunoscut" }, { status: 400 });
const client = await EterraClient.create(username, password);
let geometry;
if (layer?.spatialFilter) {
geometry = await fetchUatGeometry(client, siruta);
}
const count = layer
? geometry
? await client.countLayerByGeometry(layer, geometry)
: await client.countLayer(layer, siruta)
: await client.countParcels(siruta);
return NextResponse.json({ count });
} catch (error) {
const message = error instanceof Error ? error.message : "Eroare server";
return NextResponse.json({ error: message }, { status: 500 });
}
}