32d3f30f9d
- 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>
223 lines
8.7 KiB
TypeScript
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`;
|
|
}
|