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