fix(geoportal-v2): handle PMTiles features without uuid id

PMTiles overview tiles may carry only object_id + siruta + cadastral_ref
(not the gis_core.GisFeature uuid). Old click handler required `id` and
silently dropped clicks. Now:

- map-viewer click handler: extract objectId AND id; require only
  siruta+cadastral_ref to dispatch. Logs the props when fields are
  missing for further diagnosis.
- feature-info-panel: skip auto-fetch when feature.id is empty; show
  basic info from tile properties + nudge user to "Citește din ANCPI"
  to populate enrichment.
- Refresh button: project orchestrator response straight into the
  panel when no gis-uuid available (no second parcela.get roundtrip).
  Falls back to detail.siruta when click came from search (which only
  returns id + cadastralRef, no siruta).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude VM
2026-05-18 21:42:40 +03:00
parent 7a22b11b54
commit e0610b0573
2 changed files with 64 additions and 14 deletions
@@ -7,7 +7,9 @@ import {
import { cn } from "@/shared/lib/utils";
export interface ClickedFeatureLite {
/** GisFeature uuid — empty when tile only has object_id */
id: string;
objectId?: string;
siruta: string;
cadastralRef: string;
layerId: string;
@@ -15,7 +17,7 @@ export interface ClickedFeatureLite {
}
interface ParcelDetail {
id: string;
id?: string;
layerId?: string;
siruta?: string;
cadastralRef?: string;
@@ -60,8 +62,17 @@ export function FeatureInfoPanel({ feature, onClose }: Props) {
const [error, setError] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
// Fetch detail when feature changes
// Fetch detail when feature changes.
// If tile carries the GisFeature uuid: use the fast path (parcela.get,
// stored enrichment, no ANCPI roundtrip). If only siruta+cadastralRef are
// available: skip auto-fetch — user clicks "Citește din ANCPI" to load.
useEffect(() => {
if (!feature.id) {
setDetail(null);
setError(null);
setLoading(false);
return;
}
let cancelled = false;
setLoading(true);
setError(null);
@@ -92,6 +103,12 @@ export function FeatureInfoPanel({ feature, onClose }: Props) {
}, [feature.id]);
const refreshFromAncpi = async () => {
const siruta = feature.siruta || detail?.siruta || "";
const cadastralRef = feature.cadastralRef || detail?.cadastralRef || "";
if (!siruta || !cadastralRef) {
setError("missing_siruta_or_cad");
return;
}
setRefreshing(true);
setError(null);
try {
@@ -99,8 +116,8 @@ export function FeatureInfoPanel({ feature, onClose }: Props) {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
siruta: feature.siruta,
cadastralRef: feature.cadastralRef,
siruta,
cadastralRef,
force: true,
}),
});
@@ -109,11 +126,26 @@ export function FeatureInfoPanel({ feature, onClose }: Props) {
setError(body.error || "refresh_failed");
return;
}
// Re-fetch parcela detail
const updated = await fetch(
`/api/gis/parcela/${encodeURIComponent(feature.id)}`,
);
if (updated.ok) setDetail(await updated.json());
const techData = await res.json().catch(() => null);
// If the tile carried a GisFeature uuid, refresh stored detail.
// Otherwise project the orchestrator response into the panel directly.
if (feature.id) {
const updated = await fetch(
`/api/gis/parcela/${encodeURIComponent(feature.id)}`,
);
if (updated.ok) setDetail(await updated.json());
} else if (techData) {
// Orchestrator returns {status, data: {…enrichment fields}} verbatim.
const inner =
(techData?.data as Record<string, unknown> | undefined) ?? techData;
setDetail({
siruta: feature.siruta,
cadastralRef: feature.cadastralRef,
areaValue: feature.areaValue,
enrichment: inner as Record<string, unknown>,
});
}
} catch {
setError("network_error");
} finally {
@@ -227,6 +259,12 @@ export function FeatureInfoPanel({ feature, onClose }: Props) {
)}
</>
)}
{!detail && !loading && !error && (
<div className="rounded-md border border-dashed bg-muted/30 p-2 text-xs text-muted-foreground">
Apasă „Citește din ANCPI" pentru a încărca detaliile parcelei.
</div>
)}
</div>
<div className="flex flex-wrap gap-1 border-t bg-muted/30 p-2">
+17 -5
View File
@@ -240,19 +240,31 @@ export const MapViewer = forwardRef<MapViewerHandle, Props>(function MapViewer(
return;
}
const p = f.properties as Record<string, unknown>;
const id = (p.id as string) ?? "";
const siruta = String(p.siruta ?? "");
const cadastralRef = String(p.cadastral_ref ?? "");
const sourceLayer =
(f as unknown as { sourceLayer?: string }).sourceLayer ?? "";
const layerId =
sourceLayer === "gis_cladiri" ? "CLADIRI_ACTIVE" : "TERENURI_ACTIVE";
if (!id || !cadastralRef) {
const id = typeof p.id === "string" ? p.id : "";
const objectId =
typeof p.object_id === "number"
? String(p.object_id)
: typeof p.object_id === "string"
? p.object_id
: "";
const siruta = String(p.siruta ?? "");
const cadastralRef = String(p.cadastral_ref ?? "");
// Need at least cadastralRef + siruta to identify the parcel.
if (!cadastralRef || !siruta) {
console.warn("[v2-click] tile props missing siruta/cadastral_ref:", p);
onFeatureClick(null);
return;
}
onFeatureClick({
id,
id, // may be empty when tile lacks uuid; panel falls back to parcel/tech
objectId,
siruta,
cadastralRef,
layerId,