feat: filter no-geom by IE status (hasLandbook), add checkIfIsIE + CF PDF APIs
QUALITY GATE TIGHTENED:
No-geometry import now requires hasLandbook=1 (imobil electronic).
This filters out immovables without carte funciara — they have no
CF data, no owners, no parcel details to extract. For Cosbuc this
reduces useful no-geom from ~1916 to ~468 (only IEs with real data).
Three-tier quality gate:
1. Active (status=1)
2. Has landbook (hasLandbook=1) — is electronic immovable [NEW]
3. Has identification (cadRef/paperLbNo/paperCadNo) OR area
CLEANUP also updated: DB cleanup now removes stale no-geom records
that don't pass the tightened gate (existing non-IE records will be
cleaned on next import run).
NEW API METHODS (eterra-client):
- checkIfIsIE(adminUnitId, paperCadNo, topNo, paperCfNo) → boolean
Calls /api/immovable/checkIfIsIE — verifies IE status per-parcel
Available for future per-item verification if needed
- getCfExtractUrl(immovablePk, workspaceId) → string
Returns URL for /api/cf/landbook/copycf/get/{pk}/{ws}/0/true
Downloads the CF extract as PDF blob (future enrichment)
UI updated: 'Filtrate' label now says 'fara CF/inactive/fara date'
to reflect the new hasLandbook filter.
This commit is contained in:
@@ -2746,7 +2746,7 @@ export function ParcelSyncModule() {
|
||||
</span>
|
||||
{noGeomScan.qualityBreakdown.empty > 0 && (
|
||||
<span>
|
||||
Filtrate (inactive/fără date):{" "}
|
||||
Filtrate (fără CF/inactive/fără date):{" "}
|
||||
<span className="font-semibold text-rose-600 dark:text-rose-400">
|
||||
{noGeomScan.qualityBreakdown.empty.toLocaleString(
|
||||
"ro-RO",
|
||||
@@ -2760,7 +2760,7 @@ export function ParcelSyncModule() {
|
||||
{includeNoGeom && (
|
||||
<p className="text-[11px] text-muted-foreground ml-7">
|
||||
{noGeomScan.qualityBreakdown.empty > 0
|
||||
? `Din ${noGeomScan.noGeomCount.toLocaleString("ro-RO")} fără geometrie, ~${noGeomScan.qualityBreakdown.useful.toLocaleString("ro-RO")} vor fi importate (active, cu identificare sau suprafață). ${noGeomScan.qualityBreakdown.empty.toLocaleString("ro-RO")} vor fi filtrate (inactive sau fără date).`
|
||||
? `Din ${noGeomScan.noGeomCount.toLocaleString("ro-RO")} fără geometrie, ~${noGeomScan.qualityBreakdown.useful.toLocaleString("ro-RO")} vor fi importate (imobile electronice cu CF). ${noGeomScan.qualityBreakdown.empty.toLocaleString("ro-RO")} vor fi filtrate (fără carte funciară, inactive sau fără date).`
|
||||
: "Vor fi importate în DB și incluse în CSV + Magic GPKG (coloana HAS_GEOMETRY=0/1)."}{" "}
|
||||
În GPKG de bază apar doar cele cu geometrie.
|
||||
</p>
|
||||
|
||||
@@ -508,6 +508,53 @@ export class EterraClient {
|
||||
return this.getRawJson(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an immovable is an "Imobil Electronic" (IE) in eTerra.
|
||||
* Uses the same endpoint the eTerra UI calls when searching by
|
||||
* topNo + paperCfNo.
|
||||
*
|
||||
* @returns true if the immovable is registered as IE, false otherwise
|
||||
*/
|
||||
async checkIfIsIE(
|
||||
adminUnitId: string | number,
|
||||
paperCadNo: number | null,
|
||||
topNo: string | number,
|
||||
paperCfNo: string | number,
|
||||
): Promise<boolean> {
|
||||
const url = `${BASE_URL}/api/immovable/checkIfIsIE`;
|
||||
const payload = {
|
||||
adminUnitId: Number(adminUnitId),
|
||||
paperCadNo: Number(paperCadNo ?? 0),
|
||||
topNo: typeof topNo === "string" ? Number(topNo.split(",")[0]) || 0 : Number(topNo),
|
||||
paperCfNo: Number(paperCfNo),
|
||||
};
|
||||
try {
|
||||
const result = await this.requestRaw<boolean | number | string>(() =>
|
||||
this.client.post(url, payload, {
|
||||
headers: { "Content-Type": "application/json;charset=UTF-8" },
|
||||
timeout: this.timeoutMs,
|
||||
}),
|
||||
);
|
||||
// API may return boolean, number (1/0), or string
|
||||
return result === true || result === 1 || String(result) === "true";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the URL for downloading a CF (carte funciară) extract PDF.
|
||||
* The eTerra UI calls this to get the landbook/CF PDF blob.
|
||||
*
|
||||
* @returns URL string (caller needs an authenticated session to fetch)
|
||||
*/
|
||||
getCfExtractUrl(
|
||||
immovablePk: string | number,
|
||||
workspaceId: string | number,
|
||||
): string {
|
||||
return `${BASE_URL}/api/cf/landbook/copycf/get/${immovablePk}/${workspaceId}/0/true`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search immovable list by exact cadastral number (identifierDetails).
|
||||
* This is the eTerra application API that the web UI uses when you type
|
||||
|
||||
@@ -403,10 +403,10 @@ export async function scanNoGeometryParcels(
|
||||
if (hasLb) qWithLandbook++;
|
||||
if (hasArea) qWithArea++;
|
||||
if (isActive) qWithActiveStatus++;
|
||||
// "Useful" = ACTIVE + has any form of identification OR area
|
||||
// Matches the import quality gate
|
||||
// "Useful" = ACTIVE + HAS_LANDBOOK (imobil electronic) + has identification OR area
|
||||
// Matches the import quality gate — only IE items are worth importing
|
||||
const hasIdentification = hasCad || hasPaperLb || hasPaperCad;
|
||||
if (isActive && (hasIdentification || hasArea)) qUseful++;
|
||||
if (isActive && hasLb && (hasIdentification || hasArea)) qUseful++;
|
||||
else qEmpty++;
|
||||
}
|
||||
|
||||
@@ -483,13 +483,14 @@ export async function syncNoGeometryParcels(
|
||||
const cadRef = (item.identifierDetails ?? "").toString().trim();
|
||||
const hasPaperLb = !!(item.paperLbNo ?? "").toString().trim();
|
||||
const hasPaperCad = !!(item.paperCadNo ?? "").toString().trim();
|
||||
const hasLandbook = typeof item.hasLandbook === "number" ? item.hasLandbook : 0;
|
||||
const hasArea =
|
||||
(typeof item.measuredArea === "number" && item.measuredArea > 0) ||
|
||||
(typeof item.legalArea === "number" && item.legalArea > 0);
|
||||
const hasIdentification = !!cadRef || hasPaperLb || hasPaperCad;
|
||||
|
||||
// Only keep items that pass the quality gate (active + identification/area)
|
||||
if (status === 1 && (hasIdentification || hasArea)) {
|
||||
// Only keep items that pass the quality gate (active + hasLandbook + identification/area)
|
||||
if (status === 1 && hasLandbook === 1 && (hasIdentification || hasArea)) {
|
||||
validImmPks.add(pk);
|
||||
}
|
||||
}
|
||||
@@ -536,8 +537,8 @@ export async function syncNoGeometryParcels(
|
||||
}
|
||||
|
||||
// 4. Filter: not yet in DB + quality gate
|
||||
// Quality: must be ACTIVE (status=1) AND have identification OR area.
|
||||
// Items that are inactive, or have no identification AND no area = noise.
|
||||
// Quality: must be ACTIVE (status=1) AND hasLandbook=1 (IE) AND have identification OR area.
|
||||
// Items without landbook are not electronic immovables — no CF data to extract.
|
||||
let filteredOut = 0;
|
||||
const candidates = allImmovables.filter((item) => {
|
||||
const cadRef = normalizeCadRef(item.identifierDetails ?? "");
|
||||
@@ -554,7 +555,14 @@ export async function syncNoGeometryParcels(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Quality gate 2: must have identification OR area
|
||||
// Quality gate 2: must be an electronic immovable (hasLandbook=1)
|
||||
const hasLandbook = typeof item.hasLandbook === "number" ? item.hasLandbook : 0;
|
||||
if (hasLandbook !== 1) {
|
||||
filteredOut++;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Quality gate 3: must have identification OR area
|
||||
const hasCadRef = !!cadRef;
|
||||
const hasPaperLb = !!(item.paperLbNo ?? "").toString().trim();
|
||||
const hasPaperCad = !!(item.paperCadNo ?? "").toString().trim();
|
||||
|
||||
Reference in New Issue
Block a user