Files
ArchiTools/src/modules/geoportal/components/feature-info-panel.tsx
T
AI Assistant 32d3f30f9d fix(geoportal): auto-refresh panel after enrichment + Comanda CF always visible
- After enrichment: panel updates immediately with returned data (no reload needed)
- "Comanda CF" button visible on any parcel with cadastral ref (not just enriched ones)
- "Descarca CF" shown when CF extract already exists in DB

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

223 lines
8.7 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { X, Loader2, Sparkles, FileDown, Download, ClipboardCopy } 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);
// Fetch feature detail
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);
// Check CF status if we have a cadastral ref
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 ?? "?"}`;
const hasEnrichment = !!e && !!e.NR_CAD;
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 detail with enrichment data directly from response
if (d.enrichment && detail) {
setDetail({ ...detail, enrichment: d.enrichment, enrichedAt: new Date().toISOString() });
} else {
// Fallback: reload from API
const oid = feature.properties.object_id ?? feature.properties.objectId;
if (oid) {
const r = await fetch(`/api/geoportal/feature?objectId=${oid}&siruta=${siruta}&sourceLayer=${feature.sourceLayer}`);
if (r.ok) {
const data = await r.json();
setDetail(data.feature);
}
}
}
setEnrichMsg("");
} else {
setEnrichMsg(d.error ?? "Eroare la enrichment");
}
} catch {
setEnrichMsg("Eroare retea");
} finally {
setEnriching(false);
}
};
const handleCopy = () => {
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);
};
return (
<div className="bg-background/95 backdrop-blur-sm border rounded-lg shadow-lg w-72 overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-3 py-2 border-b">
<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">
{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 && (
<>
<Row label="SIRUTA" value={siruta} />
<Row label="Nr. cadastral" value={e?.NR_CAD ?? cadRef} />
<Row label="Nr. CF" value={e?.NR_CF} />
<Row label="Suprafata" value={formatArea(e?.SUPRAFATA_2D ?? feature.properties.area_value)} />
{hasEnrichment && (
<>
{e?.PROPRIETARI && e.PROPRIETARI !== "-" && <Row label="Proprietari" value={e.PROPRIETARI} />}
{e?.INTRAVILAN && e.INTRAVILAN !== "-" && <Row label="Intravilan" value={e.INTRAVILAN} />}
{e?.CATEGORIE_FOLOSINTA && e.CATEGORIE_FOLOSINTA !== "-" && <Row label="Categorie" value={e.CATEGORIE_FOLOSINTA} />}
</>
)}
{/* Action buttons */}
<div className="flex gap-1.5 pt-2 border-t mt-2">
{!hasEnrichment && (
<Button
variant="outline" size="sm" className="h-7 text-xs gap-1 flex-1"
onClick={handleEnrich} disabled={enriching}
title="Obtine date detaliate (proprietari, CF, categorie) de la eTerra. Datele se salveaza permanent in baza de date."
>
{enriching ? <Loader2 className="h-3 w-3 animate-spin" /> : <Sparkles className="h-3 w-3" />}
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={handleCopy} title="Copiaza informatiile in clipboard"
>
<ClipboardCopy className="h-3 w-3" />
</Button>
</div>
{enrichMsg && (
<p className="text-xs text-muted-foreground mt-1">{enrichMsg}</p>
)}
</>
)}
</div>
</div>
);
}
function Row({ label, value }: { label: string; value: unknown }) {
if (!value || value === "-" || value === "") 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 truncate">{String(value)}</span>
</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`;
}