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:
@@ -0,0 +1,87 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
EterraClient,
|
||||
type EsriGeometry,
|
||||
} from "@/modules/parcel-sync/services/eterra-client";
|
||||
import { LAYER_CATALOG } 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;
|
||||
layerIds?: string[]; // subset — omit to count all
|
||||
};
|
||||
|
||||
/**
|
||||
* POST — Count features per layer on the remote eTerra server.
|
||||
*/
|
||||
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 client = await EterraClient.create(username, password);
|
||||
let uatGeometry: EsriGeometry | undefined;
|
||||
|
||||
// Pre-fetch UAT geometry for spatial layers
|
||||
try {
|
||||
uatGeometry = await fetchUatGeometry(client, siruta);
|
||||
} catch {
|
||||
// Some layers don't need it
|
||||
}
|
||||
|
||||
const layers = body.layerIds
|
||||
? LAYER_CATALOG.filter((l) => body.layerIds!.includes(l.id))
|
||||
: LAYER_CATALOG;
|
||||
|
||||
const results: Record<string, { count: number; error?: string }> = {};
|
||||
|
||||
// Count layers in parallel, max 4 concurrent
|
||||
const chunks: (typeof layers)[] = [];
|
||||
for (let i = 0; i < layers.length; i += 4) {
|
||||
chunks.push(layers.slice(i, i + 4));
|
||||
}
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const promises = chunk.map(async (layer) => {
|
||||
try {
|
||||
const count =
|
||||
layer.spatialFilter && uatGeometry
|
||||
? await client.countLayerByGeometry(layer, uatGeometry)
|
||||
: await client.countLayer(layer, siruta);
|
||||
results[layer.id] = { count };
|
||||
} catch (err) {
|
||||
results[layer.id] = {
|
||||
count: 0,
|
||||
error: err instanceof Error ? err.message : "eroare",
|
||||
};
|
||||
}
|
||||
});
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
return NextResponse.json({ siruta, counts: results });
|
||||
} 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