feat(geoportal-v2): cladire characteristics + eterra.live link + collapsible Înscriere

Per Marius's iteration on the panel:

1. AUTO-ENRICH DISABLED — the panel no longer fires /parcel/enrich on
   sparse-data load. Refresh only via the explicit "Actualizează"
   button in the Date eTerra header. Keeps the eTerra account pool
   safe from browse-spam (was draining 500/h on rapid clicks).

2. eterra.live link button — sits beside "Actualizează" in the Date
   eTerra header. Opens https://eterra.live/harta?siruta=...&cad=...
   in a new tab so the user can cross-check the full eterra.live
   panel.

3. ÎNSCRIERE collapsible — Tip înscriere / Data cererii / Act
   proprietate now hide inside a <details> closed by default (per
   the highlighted-block screenshot). Keeps the "above the fold"
   info trimmed to what matters at a glance.

4. CARACTERISTICI CORP — new section, only rendered for
   CLADIRI_ACTIVE clicks. Shows the cladire-specific enrichment
   fields the orchestrator populates after a deep-enrich:
     - Chip row: tip / destinație / subtype (chips) + Condominium
       chip with unit count + Cu/Fără acte status pill
     - 3-col metric strip: Regim înălțime / Niveluri / An construire
     - Suprafață CF, CF IE, Clasă energetică, Părți comune (rows)
     - Observații (multi-line InfoBlock)

   Fields wired: CLADIRE_TYPE, CLADIRE_DESTINATIE, CLADIRE_SUBTYPE,
   CLADIRE_REGIM, CLADIRE_NIVELURI, CLADIRE_AN_CONSTRUIRE,
   CLADIRE_AREA_CF, CLADIRE_OBSERVATII, CLADIRE_LANDBOOK_IE,
   CLADIRE_COMMON_PARTS, CLADIRE_UNITS_NO_ANCPI,
   CLADIRE_ENERGETIC_CLASS, IS_LEGAL_BUILDING, IS_CONDOMINIUM.
   All show only when populated (no empty "-" rows). LABEL map
   extended with Romanian translations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude VM
2026-05-20 07:36:01 +03:00
parent 4f38fd1070
commit 3004790ad2
+174 -26
View File
@@ -6,7 +6,7 @@ import {
X, RefreshCw, Loader2, FileText, Download, AlertCircle, X, RefreshCw, Loader2, FileText, Download, AlertCircle,
Home, Building, Building2, MapPin, ChevronRight, Users, Home, Building, Building2, MapPin, ChevronRight, Users,
Sparkles, User, ShieldCheck, AlertTriangle, HelpCircle, Sparkles, User, ShieldCheck, AlertTriangle, HelpCircle,
Factory, Warehouse, Factory, Warehouse, ExternalLink, Hash, Layers, CalendarDays,
} from "lucide-react"; } from "lucide-react";
import { cn } from "@/shared/lib/utils"; import { cn } from "@/shared/lib/utils";
@@ -98,6 +98,23 @@ const LABEL: Record<string, string> = {
UAT: "UAT", UAT: "UAT",
UAT_SIRUTA: "SIRUTA", UAT_SIRUTA: "SIRUTA",
PARCEL_TECH_ENRICHED_AT: "Actualizat tehnic", PARCEL_TECH_ENRICHED_AT: "Actualizat tehnic",
// Cladire-specific (CLADIRI_ACTIVE)
CLADIRE_TYPE: "Tip clădire",
CLADIRE_DESTINATIE: "Destinație",
CLADIRE_DESTINATIE_CODE: "Cod destinație",
CLADIRE_SUBTYPE: "Subtip",
CLADIRE_REGIM: "Regim înălțime",
CLADIRE_NIVELURI: "Niveluri",
CLADIRE_AN_CONSTRUIRE: "An construire",
CLADIRE_AREA_CF: "Suprafață CF",
CLADIRE_LANDBOOK_IE: "Carte funciară IE",
CLADIRE_COMMON_PARTS: "Părți comune",
CLADIRE_OBSERVATII: "Observații",
CLADIRE_UNITS_NO_ANCPI: "Nr. unități ANCPI",
CLADIRE_ENERGETIC_CLASS: "Clasă energetică",
IS_LEGAL_BUILDING: "Clădire legală",
IS_CONDOMINIUM: "Condominium",
ENRICHED_AT: "Actualizat",
}; };
// ────────────────────────────────────────────────────────── formatters // ────────────────────────────────────────────────────────── formatters
@@ -657,29 +674,10 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa
], ],
); );
// Auto-enrich on sparse data // Auto-enrich on sparse data — DISABLED per Marius. Refresh fires
useEffect(() => { // only when the user explicitly hits the "Actualizează" button in
if (basic || !detail || refreshing) return; // the Date eTerra header. Keeps the eTerra account pool out of
const e = (detail.enrichment ?? {}) as Record<string, unknown>; // browse-spam territory.
const richPresent = Boolean(e.NR_CF || e.ADRESA || e.PROPRIETARI);
if (richPresent) return;
const parcelKey =
String(detail.id ?? "") ||
`${feature.siruta}:${feature.cadastralRef}:${feature.layerId}`;
const ssKey = `gis_auto_enrich_${parcelKey}`;
if (typeof sessionStorage === "undefined") return;
if (sessionStorage.getItem(ssKey)) return;
sessionStorage.setItem(ssKey, "1");
void refreshFromAncpi();
}, [
basic,
detail,
refreshing,
feature.siruta,
feature.cadastralRef,
feature.layerId,
refreshFromAncpi,
]);
const handleBuildingSelect = (b: BuildingItem) => { const handleBuildingSelect = (b: BuildingItem) => {
if (!onSelectFeature) return; if (!onSelectFeature) return;
@@ -746,6 +744,40 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa
? buildings.length ? buildings.length
: hasBuildingFlag; : hasBuildingFlag;
// Cladire-specific (only meaningful for CLADIRI_ACTIVE)
const cladireType = String(enrichment.CLADIRE_TYPE ?? "").trim();
const cladireDestinatie = String(enrichment.CLADIRE_DESTINATIE ?? "").trim();
const cladireSubtype = String(enrichment.CLADIRE_SUBTYPE ?? "").trim();
const cladireRegim = String(enrichment.CLADIRE_REGIM ?? "").trim();
const cladireNiveluri = enrichment.CLADIRE_NIVELURI;
const cladireAn = String(enrichment.CLADIRE_AN_CONSTRUIRE ?? "").trim();
const cladireAreaCf = formatArea(enrichment.CLADIRE_AREA_CF);
const cladireObservatii = String(enrichment.CLADIRE_OBSERVATII ?? "").trim();
const cladireLandbookIe = String(enrichment.CLADIRE_LANDBOOK_IE ?? "").trim();
const cladireCommonParts = String(enrichment.CLADIRE_COMMON_PARTS ?? "").trim();
const cladireUnitsNo = enrichment.CLADIRE_UNITS_NO_ANCPI;
const cladireEnergetic = String(enrichment.CLADIRE_ENERGETIC_CLASS ?? "").trim();
const isLegalBuilding = enrichment.IS_LEGAL_BUILDING;
const isCondominium = Number(enrichment.IS_CONDOMINIUM ?? 0) === 1;
const hasCladireData =
isCladiri &&
Boolean(
cladireType ||
cladireDestinatie ||
cladireSubtype ||
cladireRegim ||
cladireAn ||
cladireAreaCf ||
cladireObservatii ||
cladireLandbookIe ||
cladireEnergetic ||
cladireNiveluri != null ||
cladireUnitsNo != null ||
isLegalBuilding != null ||
isCondominium,
);
const isActive = detail?.isActive !== false; const isActive = detail?.isActive !== false;
const cadrefHeader = feature.cadastralRef || feature.objectId || "—"; const cadrefHeader = feature.cadastralRef || feature.objectId || "—";
@@ -882,12 +914,13 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa
<Sparkles className="h-2.5 w-2.5" /> <Sparkles className="h-2.5 w-2.5" />
Date eTerra Date eTerra
</p> </p>
<div className="ml-auto flex items-center gap-1">
<button <button
type="button" type="button"
onClick={() => refreshFromAncpi({ manual: true })} onClick={() => refreshFromAncpi({ manual: true })}
disabled={refreshing || !feature.siruta || !feature.cadastralRef} disabled={refreshing || !feature.siruta || !feature.cadastralRef}
className={cn( className={cn(
"ml-auto inline-flex h-7 items-center gap-1.5 rounded px-2 text-[11px] font-medium transition-colors", "inline-flex h-7 items-center gap-1.5 rounded px-2 text-[11px] font-medium transition-colors",
"border bg-background hover:bg-muted disabled:opacity-50", "border bg-background hover:bg-muted disabled:opacity-50",
)} )}
> >
@@ -903,6 +936,17 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa
</span> </span>
)} )}
</button> </button>
<a
href={`https://eterra.live/harta?siruta=${encodeURIComponent(feature.siruta)}&cad=${encodeURIComponent(feature.cadastralRef)}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex h-7 items-center gap-1 rounded border bg-background px-2 text-[11px] font-medium hover:bg-muted"
title="Deschide parcela în eterra.live"
>
<ExternalLink className="h-3 w-3 shrink-0" />
eterra.live
</a>
</div>
</div> </div>
<div className={cn("space-y-2 px-2.5 py-2", refreshing && "animate-pulse opacity-70")}> <div className={cn("space-y-2 px-2.5 py-2", refreshing && "animate-pulse opacity-70")}>
@@ -964,11 +1008,19 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa
)} )}
{(tipInscriere || dataCererii || actProp) && ( {(tipInscriere || dataCererii || actProp) && (
<div className="space-y-1 border-t pt-2"> <details className="group border-t pt-2">
<summary className="-mx-1 flex cursor-pointer list-none items-center gap-1 rounded px-1 py-0.5 hover:bg-muted/40">
<ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground transition-transform group-open:rotate-90" />
<p className="text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
Înscriere
</p>
</summary>
<div className="mt-1 space-y-1 pl-4">
{tipInscriere && <InfoRow label="Tip înscriere" value={tipInscriere} />} {tipInscriere && <InfoRow label="Tip înscriere" value={tipInscriere} />}
{dataCererii && <InfoRow label="Data cererii" value={dataCererii} />} {dataCererii && <InfoRow label="Data cererii" value={dataCererii} />}
{actProp && <InfoRow label="Act proprietate" value={actProp} />} {actProp && <InfoRow label="Act proprietate" value={actProp} />}
</div> </div>
</details>
)} )}
</div> </div>
</div> </div>
@@ -992,6 +1044,102 @@ export function FeatureInfoPanel({ feature, onClose, onSelectFeature, basic = fa
</div> </div>
)} )}
{/* Caracteristici corp (cladiri only) */}
{!loading && isCladiri && hasCladireData && (
<div className="mx-2 my-2 rounded-md border bg-muted/15">
<div className="border-b px-2.5 py-1.5">
<p className="flex items-center gap-1 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
<Building2 className="h-2.5 w-2.5" />
Caracteristici corp
</p>
</div>
<div className="space-y-2 px-2.5 py-2">
{/* Tip + destinatie + subtype as chips */}
<div className="flex flex-wrap items-center gap-1.5">
{cladireType && <Chip>{cladireType}</Chip>}
{cladireDestinatie && (
<Chip title={`Destinație: ${cladireDestinatie}`}>{cladireDestinatie}</Chip>
)}
{cladireSubtype && cladireSubtype !== cladireDestinatie && (
<Chip tone="muted">{cladireSubtype}</Chip>
)}
{isCondominium && (
<Chip
tone="default"
icon={<Building2 className="h-3 w-3" />}
>
Condominium{cladireUnitsNo != null && ` · ${cladireUnitsNo} u.`}
</Chip>
)}
{Number(isLegalBuilding) === 1 && (
<Chip
tone="success"
icon={<ShieldCheck className="h-3 w-3" />}
>
Cu acte
</Chip>
)}
{Number(isLegalBuilding) === 0 && (
<Chip
tone="warning"
icon={<AlertTriangle className="h-3 w-3" />}
>
Fără acte
</Chip>
)}
</div>
{/* Grid 3-col metrics: regim / niveluri / an */}
{(cladireRegim || cladireNiveluri != null || cladireAn) && (
<div className="grid grid-cols-3 gap-2 rounded-md bg-muted/30 px-1 py-1.5 text-center">
<div className="flex flex-col">
<span className="text-[9px] uppercase tracking-wider text-muted-foreground" title="Regim înălțime">
Regim
</span>
<span className="font-mono text-xs font-semibold">
{cladireRegim || "—"}
</span>
</div>
<div className="flex flex-col">
<span className="text-[9px] uppercase tracking-wider text-muted-foreground">
Niveluri
</span>
<span className="font-mono text-xs font-semibold tabular-nums">
{cladireNiveluri != null && cladireNiveluri !== ""
? String(cladireNiveluri)
: "—"}
</span>
</div>
<div className="flex flex-col">
<span className="text-[9px] uppercase tracking-wider text-muted-foreground">
An
</span>
<span className="font-mono text-xs font-semibold tabular-nums">
{cladireAn || "—"}
</span>
</div>
</div>
)}
{cladireAreaCf && (
<InfoRow label="Suprafață CF" value={cladireAreaCf} />
)}
{cladireLandbookIe && (
<InfoRow label="CF IE" value={cladireLandbookIe} mono />
)}
{cladireEnergetic && (
<InfoRow label="Clasă energetică" value={cladireEnergetic} />
)}
{cladireCommonParts && (
<InfoRow label="Părți comune" value={cladireCommonParts} />
)}
{cladireObservatii && (
<InfoBlock label="Observații" value={cladireObservatii} />
)}
</div>
</div>
)}
{/* Apartamente (cladiri only) */} {/* Apartamente (cladiri only) */}
{!loading && isCladiri && (condoLoading || (condoOwners && condoOwners.length > 0)) && ( {!loading && isCladiri && (condoLoading || (condoOwners && condoOwners.length > 0)) && (
<div className="mx-2 my-2"> <div className="mx-2 my-2">