19bed6724b
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>
248 lines
10 KiB
TypeScript
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`;
|
|
}
|