feat(parcel-sync): owner name search (proprietar) in Search tab

- New search mode toggle: Nr. Cadastral / Proprietar
- Owner search queries:
  1. Local DB first (enrichment PROPRIETARI/PROPRIETARI_VECHI ILIKE)
  2. eTerra API fallback (tries personName/titularName/ownerName filter keys)
- DB search works offline (no eTerra connection needed) — uses enriched data
- New API route: POST /api/eterra/search-owner
- New eterra-client method: searchImmovableByOwnerName()
- Owner results show source badge (DB local / eTerra online)
- Results can be added to saved list and exported as CSV
- Relaxed search tab guard: only requires UAT selection (not eTerra connection)
- Cadastral search still requires eTerra connection (shows hint when offline)
This commit is contained in:
AI Assistant
2026-03-08 03:48:23 +02:00
parent 8bb4a47ac5
commit 6558c690f5
3 changed files with 855 additions and 39 deletions
@@ -643,6 +643,86 @@ export class EterraClient {
);
}
/**
* Search immovable list by owner/titular name.
*
* Uses the same `/api/immovable/list` endpoint, with a `personName`
* filter key. eTerra supports partial matching on this field.
*
* Falls back to `titularName` key if personName returns empty.
*
* @returns Paginated response with `content[]`, `totalPages`, `totalElements`
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async searchImmovableByOwnerName(
workspaceId: string | number,
adminUnitId: string | number,
ownerName: string,
page = 0,
size = 20,
): Promise<any> {
const url = `${BASE_URL}/api/immovable/list`;
const baseFilters: Array<{
value: string | number;
type: "NUMBER" | "STRING";
key: string;
op: string;
}> = [
{
value: Number(workspaceId),
type: "NUMBER",
key: "workspace.nomenPk",
op: "=",
},
{
value: Number(adminUnitId),
type: "NUMBER",
key: "adminUnit.nomenPk",
op: "=",
},
{ value: -1, type: "NUMBER", key: "inscrisCF", op: "=" },
];
// Try primary key: personName (used in some eTerra versions)
const keysToTry = ["personName", "titularName", "ownerName"];
for (const filterKey of keysToTry) {
try {
const filters = [
...baseFilters,
{
value: ownerName,
type: "STRING" as const,
key: filterKey,
op: "=",
},
];
const payload = { filters, nrElements: size, page, sorters: [] };
const result = await this.requestRaw(() =>
this.client.post(url, payload, {
headers: { "Content-Type": "application/json;charset=UTF-8" },
timeout: this.timeoutMs,
}),
);
// If we got content back (even empty), it means the key is valid
if (result && typeof result === "object" && "content" in result) {
console.log(
`[searchByOwner] Key "${filterKey}" worked — ${(result as any).totalElements ?? 0} results`,
);
return result;
}
} catch (e) {
console.log(
`[searchByOwner] Key "${filterKey}" failed:`,
e instanceof Error ? e.message : e,
);
// Try next key
}
}
// All keys failed — return empty result
return { content: [], totalElements: 0, totalPages: 0 };
}
/**
* Fetch all counties (workspaces) from eTerra nomenclature.
* Returns array of { nomenPk, name, parentNomenPk, ... }