feat(parcel-sync): quality gate filter for no-geom import + diagnostic endpoint

- Filter no-geom items before import: must have identification (cadRef/CF/paperCad/paperLb) OR area
- Multi-field area extraction: area, measuredArea, areaValue, suprafata
- Scan quality breakdown: withCadRef, withPaperCf, withPaperCad, withArea, useful, empty
- Added paperLbNo to quality analysis and samples
- UI: quality breakdown grid in scan card
- UI: filtered count in workflow preview (shows useful, not total)
- UI: enrichment estimate uses useful count
- New diagnostic endpoint /api/eterra/no-geom-debug for field inspection
This commit is contained in:
AI Assistant
2026-03-07 19:58:43 +02:00
parent 681b52e816
commit af2631920f
3 changed files with 248 additions and 15 deletions
+173
View File
@@ -0,0 +1,173 @@
/**
* POST /api/eterra/no-geom-debug
*
* Diagnostic endpoint: fetches a small sample of no-geometry immovables
* and returns ALL their raw fields, so we can understand what data is
* available for quality filtering.
*
* Body: { siruta: string, sampleSize?: number }
* Returns: { sample: [...raw items], allFields: [...field names], fieldStats: {...} }
*/
import { NextResponse } from "next/server";
import { EterraClient } from "@/modules/parcel-sync/services/eterra-client";
import { getSessionCredentials } from "@/modules/parcel-sync/services/session-store";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function POST(req: Request) {
try {
const body = (await req.json()) as {
siruta?: string;
sampleSize?: number;
};
const siruta = String(body.siruta ?? "").trim();
if (!/^\d+$/.test(siruta)) {
return NextResponse.json(
{ error: "SIRUTA must be numeric" },
{ status: 400 },
);
}
const session = getSessionCredentials();
const username = String(
session?.username || process.env.ETERRA_USERNAME || "",
).trim();
const password = String(
session?.password || process.env.ETERRA_PASSWORD || "",
).trim();
if (!username || !password) {
return NextResponse.json(
{ error: "Nu ești conectat la eTerra" },
{ status: 401 },
);
}
const client = await EterraClient.create(username, password);
// Fetch first page of immovables
const response = await client.fetchImmovableListByAdminUnit(
// We need workspace PK — try to resolve
0, // placeholder, we'll get from the sample
siruta,
0,
20,
true, // inscrisCF = -1
);
// If workspace 0 doesn't work, try to resolve it
let items = response?.content ?? [];
if (items.length === 0) {
// Try without inscrisCF filter
const response2 = await client.fetchImmovableListByAdminUnit(
0,
siruta,
0,
20,
false,
);
items = response2?.content ?? [];
}
// If still empty, try to get workspace from GIS layer
if (items.length === 0) {
const features = await client.fetchAllLayer(
{
id: "TERENURI_ACTIVE",
name: "TERENURI_ACTIVE",
endpoint: "aut" as const,
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
},
siruta,
{ returnGeometry: false, outFields: "WORKSPACE_ID", pageSize: 1 },
);
const wsId = features?.[0]?.attributes?.WORKSPACE_ID;
if (wsId) {
const response3 = await client.fetchImmovableListByAdminUnit(
Number(wsId),
siruta,
0,
20,
true,
);
items = response3?.content ?? [];
}
}
if (items.length === 0) {
return NextResponse.json({
error: "Nu s-au găsit imobile",
raw: response,
});
}
// Analyze ALL fields across all items
const allFields = new Set<string>();
const fieldStats: Record<
string,
{ present: number; nonNull: number; nonEmpty: number; sample: unknown }
> = {};
for (const item of items) {
if (typeof item !== "object" || item == null) continue;
for (const [key, value] of Object.entries(item)) {
allFields.add(key);
if (!fieldStats[key]) {
fieldStats[key] = {
present: 0,
nonNull: 0,
nonEmpty: 0,
sample: undefined,
};
}
const stat = fieldStats[key]!;
stat.present++;
if (value != null) {
stat.nonNull++;
if (stat.sample === undefined) stat.sample = value;
}
if (value != null && value !== "" && value !== 0) stat.nonEmpty++;
}
}
// Also try to get GIS features for comparison
const gisFeatures = await client.fetchAllLayer(
{
id: "TERENURI_ACTIVE",
name: "TERENURI_ACTIVE",
endpoint: "aut" as const,
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
},
siruta,
{
returnGeometry: false,
outFields:
"OBJECTID,NATIONAL_CADASTRAL_REFERENCE,IMMOVABLE_ID,AREA_VALUE",
pageSize: 5,
},
);
const gisFieldStats: Record<string, unknown> = {};
if (gisFeatures.length > 0) {
const sample = gisFeatures[0]!.attributes;
for (const [key, value] of Object.entries(sample ?? {})) {
gisFieldStats[key] = value;
}
}
const sampleSize = Math.min(body.sampleSize ?? 5, items.length);
return NextResponse.json({
totalItems: response?.totalElements ?? items.length,
totalPages: response?.totalPages ?? 1,
sampleCount: sampleSize,
sample: items.slice(0, sampleSize),
allFields: Array.from(allFields).sort(),
fieldStats,
gisSampleFields: gisFieldStats,
});
} catch (error) {
const message = error instanceof Error ? error.message : "Eroare server";
return NextResponse.json({ error: message }, { status: 500 });
}
}