feat(parcel-sync): full GPKG export workflow with UAT autocomplete, hero buttons, layer catalog
- Fix login button (return success instead of ok) - Add UAT autocomplete with NFD-normalized search (3186 entries) - Add export-bundle API: base mode (terenuri+cladiri) + magic mode (enriched parcels) - Add export-layer-gpkg API: individual layer GPKG download - Add gpkg-export service: ogr2ogr with GeoJSON fallback - Add reproject service: EPSG:3844 projection support - Add magic-mode methods to eterra-client (immApps, folosinte, immovableList, docs, parcelDetails) - Rewrite UI: 3-tab layout (Export/Catalog/Search), progress tracking, phase trail
This commit is contained in:
@@ -405,6 +405,64 @@ export class EterraClient {
|
||||
return this.getLayerFields(layer);
|
||||
}
|
||||
|
||||
/* ---- Magic-mode methods (eTerra application APIs) ------------- */
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async fetchImmAppsByImmovable(immovableId: string | number, workspaceId: string | number): Promise<any[]> {
|
||||
const url = `${BASE_URL}/api/immApps/byImm/list/${immovableId}/${workspaceId}`;
|
||||
return this.getRawJson(url);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async fetchParcelFolosinte(workspaceId: string | number, immovableId: string | number, applicationId: string | number, page = 1): Promise<any[]> {
|
||||
const url = `${BASE_URL}/api/immApps/parcels/list/${workspaceId}/${immovableId}/${applicationId}/${page}`;
|
||||
return this.getRawJson(url);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async fetchImmovableListByAdminUnit(workspaceId: string | number, adminUnitId: string | number, page = 0, size = 200, includeInscrisCF = true): Promise<any> {
|
||||
const url = `${BASE_URL}/api/immovable/list`;
|
||||
const filters: 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: "C", type: "STRING", key: "immovableType", op: "<>C" },
|
||||
];
|
||||
if (includeInscrisCF) {
|
||||
filters.push({ value: -1, type: "NUMBER", key: "inscrisCF", op: "=" });
|
||||
}
|
||||
const payload = { filters, nrElements: size, page, sorters: [] };
|
||||
return this.requestRaw(() =>
|
||||
this.client.post(url, payload, {
|
||||
headers: { "Content-Type": "application/json;charset=UTF-8" },
|
||||
timeout: this.timeoutMs,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async fetchDocumentationData(workspaceId: string | number, immovableIds: Array<string | number>): Promise<any> {
|
||||
const url = `${BASE_URL}/api/documentation/data/`;
|
||||
const payload = {
|
||||
workflowCode: "EXPLORE_DATABASE",
|
||||
activityCode: "EXPLORE",
|
||||
applicationId: 0,
|
||||
workspaceId: Number(workspaceId),
|
||||
immovables: immovableIds.map((id) => Number(id)).filter(Number.isFinite),
|
||||
};
|
||||
return this.requestRaw(() =>
|
||||
this.client.post(url, payload, {
|
||||
headers: { "Content-Type": "application/json;charset=UTF-8" },
|
||||
timeout: this.timeoutMs,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async fetchImmovableParcelDetails(workspaceId: string | number, immovableId: string | number, page = 1, size = 1): Promise<any[]> {
|
||||
const url = `${BASE_URL}/api/immovable/details/parcels/list/${workspaceId}/${immovableId}/${page}/${size}`;
|
||||
return this.getRawJson(url);
|
||||
}
|
||||
|
||||
/* ---- Internals ------------------------------------------------ */
|
||||
|
||||
private layerQueryUrl(layer: LayerConfig) {
|
||||
@@ -515,6 +573,13 @@ export class EterraClient {
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private async getRawJson<T = any>(url: string): Promise<T> {
|
||||
return this.requestRaw(() =>
|
||||
this.client.get(url, { timeout: this.timeoutMs }),
|
||||
);
|
||||
}
|
||||
|
||||
private async postJson(
|
||||
url: string,
|
||||
body: URLSearchParams,
|
||||
@@ -558,6 +623,34 @@ export class EterraClient {
|
||||
return data;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private async requestRaw<T = any>(
|
||||
request: () => Promise<{ data: T | string; status: number }>,
|
||||
): Promise<T> {
|
||||
let response;
|
||||
try {
|
||||
response = await this.requestWithRetry(request);
|
||||
} catch (error) {
|
||||
const err = error as AxiosError;
|
||||
if (err?.response?.status === 401 && !this.reloginAttempted) {
|
||||
this.reloginAttempted = true;
|
||||
await this.login(this.username, this.password);
|
||||
response = await this.requestWithRetry(request);
|
||||
} else if (err?.response?.status === 401) {
|
||||
throw new Error("Session expired (401)");
|
||||
} else throw error;
|
||||
}
|
||||
const data = response.data as T | string;
|
||||
if (typeof data === "string") {
|
||||
try {
|
||||
return JSON.parse(data) as T;
|
||||
} catch {
|
||||
throw new Error("Session expired or invalid response from eTerra");
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private async queryLayer(
|
||||
layer: LayerConfig,
|
||||
params: URLSearchParams,
|
||||
|
||||
Reference in New Issue
Block a user