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,106 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { PrismaClient, type Prisma } from "@prisma/client";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
type Body = {
|
||||
siruta?: string;
|
||||
layerId?: string;
|
||||
search?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* List features stored in local GIS database with pagination & search.
|
||||
*/
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = (await req.json()) as Body;
|
||||
const siruta = String(body.siruta ?? "").trim();
|
||||
const layerId = String(body.layerId ?? "").trim();
|
||||
const search = (body.search ?? "").trim();
|
||||
const page = Math.max(1, body.page ?? 1);
|
||||
const pageSize = Math.min(200, Math.max(1, body.pageSize ?? 50));
|
||||
|
||||
if (!siruta) {
|
||||
return NextResponse.json(
|
||||
{ error: "SIRUTA obligatoriu" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const where: Prisma.GisFeatureWhereInput = { siruta };
|
||||
if (layerId) where.layerId = layerId;
|
||||
if (body.projectId) where.projectId = body.projectId;
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ cadastralRef: { contains: search, mode: "insensitive" } },
|
||||
{ inspireId: { contains: search, mode: "insensitive" } },
|
||||
];
|
||||
}
|
||||
|
||||
const [features, total] = await Promise.all([
|
||||
prisma.gisFeature.findMany({
|
||||
where,
|
||||
select: {
|
||||
id: true,
|
||||
layerId: true,
|
||||
siruta: true,
|
||||
objectId: true,
|
||||
inspireId: true,
|
||||
cadastralRef: true,
|
||||
areaValue: true,
|
||||
isActive: true,
|
||||
attributes: true,
|
||||
projectId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
// geometry omitted for list — too large; fetch single feature by ID for geometry
|
||||
},
|
||||
orderBy: { objectId: "asc" },
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
}),
|
||||
prisma.gisFeature.count({ where }),
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
features,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Eroare server";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/eterra/features?id=... — Single feature with full geometry.
|
||||
*/
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const url = new URL(req.url);
|
||||
const id = url.searchParams.get("id");
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: "ID obligatoriu" }, { status: 400 });
|
||||
}
|
||||
|
||||
const feature = await prisma.gisFeature.findUnique({ where: { id } });
|
||||
if (!feature) {
|
||||
return NextResponse.json({ error: "Negăsit" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(feature);
|
||||
} 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