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";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import {
|
||||
X, RefreshCw, Loader2, FileText, Download, AlertCircle,
|
||||
@@ -409,7 +409,7 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
||||
};
|
||||
}, [isCladiri, feature.siruta, feature.cadastralRef, basic]);
|
||||
|
||||
const refreshFromAncpi = async () => {
|
||||
const refreshFromAncpi = useCallback(async () => {
|
||||
if (!feature.siruta || !feature.cadastralRef) {
|
||||
setError("missing_siruta_or_cad");
|
||||
return;
|
||||
@@ -432,15 +432,26 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
||||
return;
|
||||
}
|
||||
const techData = await res.json().catch(() => null);
|
||||
// Refresh stored detail via parcela.get if uuid known.
|
||||
if (detail?.id || feature.id) {
|
||||
const id = detail?.id ?? feature.id;
|
||||
const updated = await fetch(
|
||||
`/api/gis/parcela/${encodeURIComponent(id)}`,
|
||||
// After orchestrator updates the central DB, re-fetch via the
|
||||
// server-side find/get path so we land on the canonical shape
|
||||
// (and pick up rich enrichment that the tech response itself
|
||||
// doesn't carry).
|
||||
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) {
|
||||
// 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 =
|
||||
(techData?.data as Record<string, unknown> | undefined) ?? techData;
|
||||
setDetail({
|
||||
@@ -456,7 +467,44 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
||||
} finally {
|
||||
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 url = `https://eterra.live/harta?siruta=${encodeURIComponent(
|
||||
@@ -585,6 +633,14 @@ export function FeatureInfoPanel({ feature, onClose, basic = false }: Props) {
|
||||
</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
|
||||
once — likely Authentik scope misconfig. Stays discreet (no
|
||||
call-to-action), only diag info for the operator. */}
|
||||
|
||||
Reference in New Issue
Block a user