feat(geoportal-v2): auto-fetch enrichment when DB only has tech keys
Parcel 328607 in Cluj-Napoca (and many others) is in gis_core with
only 10 enrichment keys, all tech-level (PARCEL_HAS_LANDBOOK,
PARCEL_IS_CONDOMINIUM, etc.) — no NR_CF, no ADRESA, no PROPRIETARI.
The panel renders correctly but with nothing of substance shown.
User had to manually click "Citește din ANCPI" to backfill.
Now auto-fires the fetch when:
- Panel mounts a fresh detail (or after parcel switch)
- AND enrichment lacks ALL three of NR_CF / ADRESA / PROPRIETARI
- AND we haven't already auto-fetched for this parcel this tab
session (sessionStorage dedupe keyed by uuid OR siruta+cad+layerId)
Visible feedback while it runs: a quiet "Se preiau date suplimentare
din ANCPI…" strip below the loading area. The user can keep reading
whatever is already on screen.
Side fixes:
- refreshFromAncpi → useCallback (stable deps) so it can sit in the
auto-trigger useEffect's dep array without infinite loops.
- Refresh path now uses /api/gis/parcela/find as the fallback when no
uuid is available, matching the initial-load logic. The old
orchestrator-shape projection still exists as a last-resort fallback
but is rarely hit.
Note: orchestrator's parcel/tech only re-populates PARCEL_* tech
fields. Truly enriching rich PII (NR_CF/ADRESA/PROPRIETARI) needs the
"deep enrich" orchestrator path which the gis-api proxy contract
doesn't expose yet — separate gis-api task. So parcels that only ever
got tech-level enrichment will stay at tech-level even after this
auto-fetch. The visible improvement is: parcels that DO have rich
data load it in the first second instead of needing a manual click.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import {
|
import {
|
||||||
X, RefreshCw, Loader2, FileText, Download, AlertCircle,
|
X, RefreshCw, Loader2, FileText, Download, AlertCircle,
|
||||||
@@ -409,7 +409,7 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
|||||||
};
|
};
|
||||||
}, [isCladiri, feature.siruta, feature.cadastralRef, basic]);
|
}, [isCladiri, feature.siruta, feature.cadastralRef, basic]);
|
||||||
|
|
||||||
const refreshFromAncpi = async () => {
|
const refreshFromAncpi = useCallback(async () => {
|
||||||
if (!feature.siruta || !feature.cadastralRef) {
|
if (!feature.siruta || !feature.cadastralRef) {
|
||||||
setError("missing_siruta_or_cad");
|
setError("missing_siruta_or_cad");
|
||||||
return;
|
return;
|
||||||
@@ -432,15 +432,26 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const techData = await res.json().catch(() => null);
|
const techData = await res.json().catch(() => null);
|
||||||
// Refresh stored detail via parcela.get if uuid known.
|
// After orchestrator updates the central DB, re-fetch via the
|
||||||
if (detail?.id || feature.id) {
|
// server-side find/get path so we land on the canonical shape
|
||||||
const id = detail?.id ?? feature.id;
|
// (and pick up rich enrichment that the tech response itself
|
||||||
const updated = await fetch(
|
// doesn't carry).
|
||||||
`/api/gis/parcela/${encodeURIComponent(id)}`,
|
const id = detail?.id ?? feature.id;
|
||||||
|
let updated: Response | null = null;
|
||||||
|
if (id) {
|
||||||
|
updated = await fetch(`/api/gis/parcela/${encodeURIComponent(id)}`);
|
||||||
|
} else {
|
||||||
|
updated = await fetch(
|
||||||
|
`/api/gis/parcela/find?siruta=${encodeURIComponent(feature.siruta)}` +
|
||||||
|
`&cad=${encodeURIComponent(feature.cadastralRef)}` +
|
||||||
|
`&layerId=${encodeURIComponent(feature.layerId)}`,
|
||||||
);
|
);
|
||||||
if (updated.ok) setDetail(await updated.json());
|
}
|
||||||
|
if (updated.ok) {
|
||||||
|
setDetail(await updated.json());
|
||||||
} else if (techData) {
|
} else if (techData) {
|
||||||
// Project the orchestrator response into the panel directly.
|
// Final fallback — project the orchestrator response if we
|
||||||
|
// can't re-fetch the canonical record.
|
||||||
const inner =
|
const inner =
|
||||||
(techData?.data as Record<string, unknown> | undefined) ?? techData;
|
(techData?.data as Record<string, unknown> | undefined) ?? techData;
|
||||||
setDetail({
|
setDetail({
|
||||||
@@ -456,7 +467,44 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
|||||||
} finally {
|
} finally {
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
};
|
}, [
|
||||||
|
feature.id,
|
||||||
|
feature.siruta,
|
||||||
|
feature.cadastralRef,
|
||||||
|
feature.layerId,
|
||||||
|
feature.areaValue,
|
||||||
|
detail?.id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Auto-fetch from ANCPI when the central DB has only tech-level keys
|
||||||
|
// (no NR_CF / ADRESA / PROPRIETARI). The orchestrator caches its results
|
||||||
|
// for 30 days so repeat clicks on the same parcel don't re-spend quota.
|
||||||
|
// Dedupe per parcel-key via sessionStorage so a remount or React
|
||||||
|
// strict-mode double-fire doesn't double the call.
|
||||||
|
useEffect(() => {
|
||||||
|
if (basic || !detail || refreshing) return;
|
||||||
|
const e = (detail.enrichment ?? {}) as Record<string, unknown>;
|
||||||
|
const richPresent = Boolean(e.NR_CF || e.ADRESA || e.PROPRIETARI);
|
||||||
|
if (richPresent) return;
|
||||||
|
|
||||||
|
const parcelKey =
|
||||||
|
String(detail.id ?? "") ||
|
||||||
|
`${feature.siruta}:${feature.cadastralRef}:${feature.layerId}`;
|
||||||
|
const ssKey = `gis_auto_enrich_${parcelKey}`;
|
||||||
|
if (typeof sessionStorage === "undefined") return;
|
||||||
|
if (sessionStorage.getItem(ssKey)) return;
|
||||||
|
sessionStorage.setItem(ssKey, "1");
|
||||||
|
|
||||||
|
void refreshFromAncpi();
|
||||||
|
}, [
|
||||||
|
basic,
|
||||||
|
detail,
|
||||||
|
refreshing,
|
||||||
|
feature.siruta,
|
||||||
|
feature.cadastralRef,
|
||||||
|
feature.layerId,
|
||||||
|
refreshFromAncpi,
|
||||||
|
]);
|
||||||
|
|
||||||
const exportGpkg = () => {
|
const exportGpkg = () => {
|
||||||
const url = `https://eterra.live/harta?siruta=${encodeURIComponent(
|
const url = `https://eterra.live/harta?siruta=${encodeURIComponent(
|
||||||
@@ -585,6 +633,14 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Auto-enrich is running in background (sparse DB record → fetched from ANCPI) */}
|
||||||
|
{refreshing && !loading && (
|
||||||
|
<div className="flex items-center gap-2 border-b bg-muted/20 px-3 py-1.5 text-[11px] text-muted-foreground">
|
||||||
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
|
<span>Se preiau date suplimentare din ANCPI…</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Forbidden after a silent re-grant attempt already failed
|
{/* Forbidden after a silent re-grant attempt already failed
|
||||||
once — likely Authentik scope misconfig. Stays discreet (no
|
once — likely Authentik scope misconfig. Stays discreet (no
|
||||||
call-to-action), only diag info for the operator. */}
|
call-to-action), only diag info for the operator. */}
|
||||||
|
|||||||
Reference in New Issue
Block a user