Files
ArchiTools/src/modules/geoportal/components/feature-info-panel.tsx
T
AI Assistant 19bed6724b fix(geoportal): enrichment panel update + force-hide all layers + boundary filter
1. Enrichment: panel now updates immediately with returned data (was only showing message)
2. Layers: ALL data layers set to visibility:none immediately after creation,
   then only enabled ones are shown. Fixes cladiri appearing when only terenuri toggled.
3. OpenFreeMap boundaries: also filter by source-layer="boundary" (more reliable)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 13:45:29 +02:00

248 lines
10 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { X, Loader2, Sparkles, FileDown, Download, ClipboardCopy, Building2, AlertTriangle } from "lucide-react";
import { Button } from "@/shared/components/ui/button";
import type { ClickedFeature, FeatureDetail, FeatureEnrichmentData } from "../types";
type CfStatus = { available: boolean; expired?: boolean; downloadUrl?: string; documentName?: string };
type FeatureInfoPanelProps = {
feature: ClickedFeature | null;
onClose: () => void;
};
export function FeatureInfoPanel({ feature, onClose }: FeatureInfoPanelProps) {
const [detail, setDetail] = useState<FeatureDetail | null>(null);
const [loading, setLoading] = useState(false);
const [enriching, setEnriching] = useState(false);
const [enrichMsg, setEnrichMsg] = useState("");
const [cfStatus, setCfStatus] = useState<CfStatus | null>(null);
useEffect(() => {
if (!feature) { setDetail(null); setCfStatus(null); return; }
const objectId = feature.properties.object_id ?? feature.properties.objectId;
const siruta = feature.properties.siruta;
if (!objectId || !siruta) { setDetail(null); return; }
let cancelled = false;
setLoading(true);
setEnrichMsg("");
setCfStatus(null);
fetch(`/api/geoportal/feature?objectId=${objectId}&siruta=${siruta}&sourceLayer=${feature.sourceLayer}`)
.then((r) => r.ok ? r.json() : Promise.reject())
.then((data: { feature: FeatureDetail }) => {
if (cancelled) return;
setDetail(data.feature);
const e = data.feature.enrichment as FeatureEnrichmentData | null;
const nrCad = e?.NR_CAD ?? data.feature.cadastralRef;
if (nrCad) {
fetch(`/api/geoportal/cf-status?nrCad=${encodeURIComponent(nrCad)}`)
.then((r) => r.ok ? r.json() : null)
.then((cf: CfStatus | null) => { if (!cancelled && cf) setCfStatus(cf); })
.catch(() => {});
}
})
.catch(() => { if (!cancelled) setDetail(null); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [feature]);
if (!feature) return null;
const e = detail?.enrichment as FeatureEnrichmentData | null | undefined;
const isUat = feature.sourceLayer?.includes("uat");
const cadRef = e?.NR_CAD ?? String(feature.properties.cadastral_ref ?? "");
const title = isUat
? String(feature.properties.name ?? "UAT")
: cadRef ? `Parcela ${cadRef}` : `#${feature.properties.object_id ?? "?"}`;
// Has REAL enrichment (not just NR_CAD/SUPRAFATA which come from basic sync)
const hasRealEnrichment = !!e && !!(e.NR_CF || e.PROPRIETARI || e.CATEGORIE_FOLOSINTA || e.INTRAVILAN);
const siruta = String(feature.properties.siruta ?? detail?.siruta ?? "");
const handleEnrich = async () => {
if (!detail?.id && !siruta) return;
setEnriching(true);
setEnrichMsg("");
try {
const objectId = feature.properties.object_id ?? feature.properties.objectId;
const resp = await fetch("/api/geoportal/enrich", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(detail?.id ? { featureId: detail.id } : { siruta, objectId: Number(objectId) }),
});
const d = await resp.json();
if (resp.ok) {
// Update panel with enrichment data immediately
if (d.enrichment && detail) {
setDetail({ ...detail, enrichment: d.enrichment, enrichedAt: new Date().toISOString() });
setEnrichMsg("");
} else {
setEnrichMsg(d.message ?? "Enrichment finalizat");
}
} else {
setEnrichMsg(d.error ?? "Eroare la enrichment");
}
} catch {
setEnrichMsg("Eroare retea");
} finally {
setEnriching(false);
}
};
return (
<div className="bg-background/95 backdrop-blur-sm border rounded-lg shadow-lg w-80 max-h-[60vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between px-3 py-2 border-b sticky top-0 bg-background/95 backdrop-blur-sm">
<h3 className="text-sm font-semibold truncate">{title}</h3>
<Button variant="ghost" size="sm" className="h-6 w-6 p-0 shrink-0 ml-2" onClick={onClose}>
<X className="h-3.5 w-3.5" />
</Button>
</div>
<div className="px-3 py-2 text-xs space-y-1.5">
{loading && (
<div className="flex items-center gap-2 text-muted-foreground py-2 justify-center">
<Loader2 className="h-3.5 w-3.5 animate-spin" /> Se incarca...
</div>
)}
{!loading && isUat && (
<>
<Row label="UAT" value={feature.properties.name} />
<Row label="SIRUTA" value={feature.properties.siruta} />
<Row label="Judet" value={feature.properties.county} />
</>
)}
{!loading && !isUat && (
<>
{/* Basic info */}
<Row label="SIRUTA" value={siruta} />
<Row label="Nr. cadastral" value={e?.NR_CAD ?? cadRef} />
<Row label="Nr. CF" value={e?.NR_CF} />
<Row label="CF vechi" value={e?.NR_CF_VECHI} />
<Row label="Nr. topo" value={e?.NR_TOPO} />
<Row label="Suprafata" value={formatArea(e?.SUPRAFATA_2D ?? feature.properties.area_value)} />
{/* Enrichment data */}
{hasRealEnrichment && (
<>
<div className="border-t pt-1.5 mt-1.5" />
<WrapRow label="Proprietari" value={e?.PROPRIETARI} />
<WrapRow label="Proprietari vechi" value={e?.PROPRIETARI_VECHI} />
<Row label="Intravilan" value={e?.INTRAVILAN} />
<Row label="Categorie" value={e?.CATEGORIE_FOLOSINTA} />
<WrapRow label="Adresa" value={e?.ADRESA} />
<Row label="Solicitant" value={e?.SOLICITANT} />
{/* Building info */}
{(e?.HAS_BUILDING === 1) && (
<div className="flex items-center gap-1.5 pt-1">
<Building2 className="h-3 w-3 text-blue-500" />
<span className="text-muted-foreground">Constructie:</span>
<span className="font-medium">
{e?.BUILD_LEGAL === 1 ? "Cu acte" : (
<span className="flex items-center gap-1 text-amber-600">
<AlertTriangle className="h-3 w-3" /> Fara acte
</span>
)}
</span>
</div>
)}
</>
)}
{/* Action buttons */}
<div className="flex gap-1.5 pt-2 border-t mt-2">
{!hasRealEnrichment && (
<Button
variant="outline" size="sm" className="h-7 text-xs gap-1 flex-1"
onClick={handleEnrich} disabled={enriching}
title="Obtine date detaliate de la eTerra (proprietari, CF, categorie). Se salveaza permanent."
>
{enriching ? <Loader2 className="h-3 w-3 animate-spin" /> : <Sparkles className="h-3 w-3" />}
{enriching ? "Se incarca..." : "Enrichment"}
</Button>
)}
{cfStatus?.available && cfStatus.downloadUrl ? (
<Button
variant="outline" size="sm" className="h-7 text-xs gap-1 flex-1"
onClick={() => window.open(cfStatus.downloadUrl, "_blank")}
title={`Descarca extras CF: ${cfStatus.documentName ?? "PDF"}`}
>
<FileDown className="h-3 w-3" />
Descarca CF
</Button>
) : cadRef ? (
<Button
variant="outline" size="sm" className="h-7 text-xs gap-1 flex-1"
onClick={() => window.open(`/parcel-sync?tab=extrase&search=${encodeURIComponent(cadRef)}`, "_blank")}
title="Comanda extras CF de la ANCPI ePay"
>
<Download className="h-3 w-3" />
Comanda CF
</Button>
) : null}
<Button
variant="outline" size="sm" className="h-7 text-xs gap-1"
onClick={() => {
const text = [
cadRef && `Nr. cad: ${cadRef}`,
e?.NR_CF && `CF: ${e.NR_CF}`,
(e?.SUPRAFATA_2D ?? feature.properties.area_value) && `S: ${e?.SUPRAFATA_2D ?? feature.properties.area_value} mp`,
e?.PROPRIETARI && e.PROPRIETARI !== "-" && `Prop: ${e.PROPRIETARI}`,
siruta && `SIRUTA: ${siruta}`,
].filter(Boolean).join("\n");
navigator.clipboard.writeText(text);
}}
title="Copiaza informatiile in clipboard"
>
<ClipboardCopy className="h-3 w-3" />
</Button>
</div>
{enrichMsg && (
<p className="text-xs text-amber-600 dark:text-amber-400 mt-1 leading-relaxed">{enrichMsg}</p>
)}
</>
)}
</div>
</div>
);
}
/** Single-line row (value truncated only if very long) */
function Row({ label, value }: { label: string; value: unknown }) {
if (!value || value === "-" || value === "" || value === 0) return null;
return (
<div className="flex justify-between gap-2">
<span className="text-muted-foreground shrink-0">{label}</span>
<span className="text-right font-medium break-words max-w-[60%]">{String(value)}</span>
</div>
);
}
/** Multi-line row (long text wraps) */
function WrapRow({ label, value }: { label: string; value: unknown }) {
if (!value || value === "-" || value === "") return null;
return (
<div>
<span className="text-muted-foreground">{label}</span>
<p className="font-medium text-xs leading-relaxed mt-0.5">{String(value)}</p>
</div>
);
}
function formatArea(v: unknown): string {
if (!v || v === "") return "";
const n = typeof v === "number" ? v : parseFloat(String(v));
if (isNaN(n)) return String(v);
return `${n.toLocaleString("ro-RO")} mp`;
}