"use client"; import { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { Loader2, FileDown, CheckCircle2, XCircle, Wifi, MapPin, Sparkles, RefreshCw, Database, HardDrive, Clock, ArrowDownToLine, AlertTriangle, } from "lucide-react"; import { Button } from "@/shared/components/ui/button"; import { Badge } from "@/shared/components/ui/badge"; import { Card, CardContent } from "@/shared/components/ui/card"; import { cn } from "@/shared/lib/utils"; import { LAYER_CATALOG, } from "../../services/eterra-layers"; import type { SessionStatus, SyncRunInfo, ExportProgress, } from "../parcel-sync-types"; import { relativeTime } from "../parcel-sync-types"; /* ------------------------------------------------------------------ */ /* Props */ /* ------------------------------------------------------------------ */ export type ExportTabProps = { siruta: string; workspacePk: number | null; sirutaValid: boolean; session: SessionStatus; syncLocalCounts: Record; syncRuns: SyncRunInfo[]; syncingSiruta: string; exporting: boolean; setExporting: (v: boolean) => void; onSyncRefresh: () => void; onDbRefresh: () => void; }; /* ------------------------------------------------------------------ */ /* No-geom scan result type */ /* ------------------------------------------------------------------ */ type NoGeomScanResult = { totalImmovables: number; withGeometry: number; remoteGisCount: number; remoteCladiriCount: number; noGeomCount: number; matchedByRef: number; matchedById: number; qualityBreakdown: { withCadRef: number; withPaperCad: number; withPaperLb: number; withLandbook: number; withArea: number; withActiveStatus: number; useful: number; empty: number; }; localDbTotal: number; localDbWithGeom: number; localDbNoGeom: number; localDbEnriched: number; localDbEnrichedComplete: number; localSyncFresh: boolean; scannedAt: string; }; /* ------------------------------------------------------------------ */ /* Component */ /* ------------------------------------------------------------------ */ export function ExportTab({ siruta, workspacePk, sirutaValid, session, syncLocalCounts, syncRuns, syncingSiruta, exporting, setExporting, onSyncRefresh, onDbRefresh, }: ExportTabProps) { /* ── Export state ──────────────────────────────────────────── */ const [exportJobId, setExportJobId] = useState(null); const [exportProgress, setExportProgress] = useState( null, ); const [phaseTrail, setPhaseTrail] = useState([]); const pollingRef = useRef | null>(null); /* ── No-geometry state ────────────────────────────────────── */ const [includeNoGeom, setIncludeNoGeom] = useState(false); const [noGeomScanning, setNoGeomScanning] = useState(false); const [noGeomScan, setNoGeomScan] = useState(null); const [noGeomScanSiruta, setNoGeomScanSiruta] = useState(""); /* ── Background sync state ────────────────────────────────── */ const [bgJobId, setBgJobId] = useState(null); const [bgProgress, setBgProgress] = useState(null); const [bgPhaseTrail, setBgPhaseTrail] = useState([]); const bgPollingRef = useRef | null>(null); const [downloadingFromDb, setDownloadingFromDb] = useState(false); /* ══════════════════════════════════════════════════════════ */ /* Derived data */ /* ══════════════════════════════════════════════════════════ */ const dbLayersSummary = useMemo(() => { if (!sirutaValid || syncingSiruta !== siruta) return []; return LAYER_CATALOG.filter((l) => (syncLocalCounts[l.id] ?? 0) > 0).map( (l) => { const count = syncLocalCounts[l.id] ?? 0; const lastRun = syncRuns.find( (r) => r.layerId === l.id && r.status === "done", ); const lastSynced = lastRun?.completedAt ? new Date(lastRun.completedAt) : null; const ageMs = lastSynced ? Date.now() - lastSynced.getTime() : null; const isFresh = ageMs !== null ? ageMs < 168 * 60 * 60 * 1000 : false; return { ...l, count, lastSynced, isFresh }; }, ); }, [sirutaValid, syncingSiruta, siruta, syncLocalCounts, syncRuns]); const dbTotalFeatures = dbLayersSummary.reduce((sum, l) => sum + l.count, 0); const progressPct = exportProgress?.total && exportProgress.total > 0 ? Math.round((exportProgress.downloaded / exportProgress.total) * 100) : 0; /* ══════════════════════════════════════════════════════════ */ /* Progress polling */ /* ══════════════════════════════════════════════════════════ */ const startPolling = useCallback((jid: string) => { if (pollingRef.current) clearInterval(pollingRef.current); pollingRef.current = setInterval(async () => { try { const res = await fetch( `/api/eterra/progress?jobId=${encodeURIComponent(jid)}`, ); const data = (await res.json()) as ExportProgress; setExportProgress(data); if (data.phase) { setPhaseTrail((prev) => { if (prev[prev.length - 1] === data.phase) return prev; return [...prev, data.phase!]; }); } if (data.status === "done" || data.status === "error") { if (pollingRef.current) { clearInterval(pollingRef.current); pollingRef.current = null; } } } catch { /* ignore polling errors */ } }, 1000); }, []); useEffect(() => { return () => { if (pollingRef.current) clearInterval(pollingRef.current); }; }, []); /* ══════════════════════════════════════════════════════════ */ /* Export bundle (base / magic) */ /* ══════════════════════════════════════════════════════════ */ const handleExportBundle = useCallback( async (mode: "base" | "magic") => { if (!siruta || exporting) return; const jobId = crypto.randomUUID(); setExportJobId(jobId); setExportProgress(null); setPhaseTrail([]); setExporting(true); startPolling(jobId); try { const res = await fetch("/api/eterra/export-bundle", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ siruta, jobId, mode, includeNoGeometry: includeNoGeom, }), }); if (!res.ok) { const err = (await res.json().catch(() => ({}))) as { error?: string; }; throw new Error(err.error ?? `HTTP ${res.status}`); } const blob = await res.blob(); const cd = res.headers.get("Content-Disposition") ?? ""; const match = /filename="?([^"]+)"?/.exec(cd); const filename = match?.[1] ?? (mode === "magic" ? `eterra_uat_${siruta}_magic.zip` : `eterra_uat_${siruta}_terenuri_cladiri.zip`); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); // Mark progress as done after successful download setExportProgress((prev) => prev ? { ...prev, status: "done", phase: "Finalizat", downloaded: prev.total ?? 100, total: prev.total ?? 100, message: `Descărcare completă — ${filename}`, note: undefined, } : null, ); } catch (error) { const msg = error instanceof Error ? error.message : "Eroare export"; setExportProgress((prev) => prev ? { ...prev, status: "error", message: msg } : { jobId, downloaded: 0, status: "error", message: msg, }, ); } if (pollingRef.current) { clearInterval(pollingRef.current); pollingRef.current = null; } setExporting(false); // Refresh sync status — data was synced to DB onSyncRefresh(); }, [siruta, exporting, startPolling, includeNoGeom, setExporting, onSyncRefresh], ); /* ══════════════════════════════════════════════════════════ */ /* No-geometry scan */ /* ══════════════════════════════════════════════════════════ */ const handleNoGeomScan = useCallback( async (targetSiruta?: string) => { const s = targetSiruta ?? siruta; if (!s) return; setNoGeomScanning(true); setNoGeomScan(null); setNoGeomScanSiruta(s); const emptyQuality = { withCadRef: 0, withPaperCad: 0, withPaperLb: 0, withLandbook: 0, withArea: 0, withActiveStatus: 0, useful: 0, empty: 0, }; const emptyResult: NoGeomScanResult = { totalImmovables: 0, withGeometry: 0, remoteGisCount: 0, remoteCladiriCount: 0, noGeomCount: 0, matchedByRef: 0, matchedById: 0, qualityBreakdown: emptyQuality, localDbTotal: 0, localDbWithGeom: 0, localDbNoGeom: 0, localDbEnriched: 0, localDbEnrichedComplete: 0, localSyncFresh: false, scannedAt: "", }; try { // 2min timeout — scan is informational, should not block the page const scanAbort = new AbortController(); const scanTimer = setTimeout(() => scanAbort.abort(), 120_000); const res = await fetch("/api/eterra/no-geom-scan", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ siruta: s, workspacePk: workspacePk ?? undefined, }), signal: scanAbort.signal, }); clearTimeout(scanTimer); const data = (await res.json()) as Record; if (data.error) { console.warn("[no-geom-scan]", data.error); setNoGeomScan(emptyResult); } else { const qb = (data.qualityBreakdown ?? {}) as Record; setNoGeomScan({ totalImmovables: Number(data.totalImmovables ?? 0), withGeometry: Number(data.withGeometry ?? 0), remoteGisCount: Number(data.remoteGisCount ?? 0), remoteCladiriCount: Number(data.remoteCladiriCount ?? 0), noGeomCount: Number(data.noGeomCount ?? 0), matchedByRef: Number(data.matchedByRef ?? 0), matchedById: Number(data.matchedById ?? 0), qualityBreakdown: { withCadRef: Number(qb.withCadRef ?? 0), withPaperCad: Number(qb.withPaperCad ?? 0), withPaperLb: Number(qb.withPaperLb ?? 0), withLandbook: Number(qb.withLandbook ?? 0), withArea: Number(qb.withArea ?? 0), withActiveStatus: Number(qb.withActiveStatus ?? 0), useful: Number(qb.useful ?? 0), empty: Number(qb.empty ?? 0), }, localDbTotal: Number(data.localDbTotal ?? 0), localDbWithGeom: Number(data.localDbWithGeom ?? 0), localDbNoGeom: Number(data.localDbNoGeom ?? 0), localDbEnriched: Number(data.localDbEnriched ?? 0), localDbEnrichedComplete: Number(data.localDbEnrichedComplete ?? 0), localSyncFresh: Boolean(data.localSyncFresh), scannedAt: String(data.scannedAt ?? new Date().toISOString()), }); } } catch (err) { // Distinguish timeout from other errors for the user const isTimeout = err instanceof DOMException && err.name === "AbortError"; if (isTimeout) { console.warn( "[no-geom-scan] Timeout after 2 min — server eTerra lent", ); } setNoGeomScan({ ...emptyResult, scannedAt: isTimeout ? "timeout" : "", }); } setNoGeomScanning(false); }, [siruta, workspacePk], ); // Auto-scan for no-geometry parcels when UAT is selected + connected const noGeomAutoScanRef = useRef(""); useEffect(() => { if (!siruta || !session.connected) return; // Don't re-scan if we already scanned (or are scanning) this siruta if (noGeomAutoScanRef.current === siruta) return; noGeomAutoScanRef.current = siruta; void handleNoGeomScan(siruta); }, [siruta, session.connected, handleNoGeomScan]); /* ══════════════════════════════════════════════════════════ */ /* Background sync polling */ /* ══════════════════════════════════════════════════════════ */ const startBgPolling = useCallback( (jid: string) => { if (bgPollingRef.current) clearInterval(bgPollingRef.current); bgPollingRef.current = setInterval(async () => { try { const res = await fetch( `/api/eterra/progress?jobId=${encodeURIComponent(jid)}`, ); const data = (await res.json()) as ExportProgress; setBgProgress(data); if (data.phase) { setBgPhaseTrail((prev) => { if (prev[prev.length - 1] === data.phase) return prev; return [...prev, data.phase!]; }); } if (data.status === "done" || data.status === "error") { if (bgPollingRef.current) { clearInterval(bgPollingRef.current); bgPollingRef.current = null; } // Clean localStorage marker try { localStorage.removeItem("parcel-sync:bg-job"); } catch { /* */ } // Refresh sync status and DB summary onSyncRefresh(); onDbRefresh(); } } catch { /* ignore polling errors */ } }, 1500); }, [onSyncRefresh, onDbRefresh], ); // Cleanup bg polling on unmount useEffect(() => { return () => { if (bgPollingRef.current) clearInterval(bgPollingRef.current); }; }, []); // Recover background job from localStorage on mount useEffect(() => { try { const raw = localStorage.getItem("parcel-sync:bg-job"); if (!raw) return; const saved = JSON.parse(raw) as { jobId: string; siruta: string; startedAt: string; }; // Ignore jobs older than 8 hours const age = Date.now() - new Date(saved.startedAt).getTime(); if (age > 8 * 60 * 60 * 1000) { localStorage.removeItem("parcel-sync:bg-job"); return; } // Check if job is still running void (async () => { try { const res = await fetch( `/api/eterra/progress?jobId=${encodeURIComponent(saved.jobId)}`, ); const data = (await res.json()) as ExportProgress; if (data.status === "running") { setBgJobId(saved.jobId); setBgProgress(data); if (data.phase) setBgPhaseTrail([data.phase]); startBgPolling(saved.jobId); } else if (data.status === "done") { setBgJobId(saved.jobId); setBgProgress(data); if (data.phase) setBgPhaseTrail(["Sincronizare completă"]); localStorage.removeItem("parcel-sync:bg-job"); } else { localStorage.removeItem("parcel-sync:bg-job"); } } catch { localStorage.removeItem("parcel-sync:bg-job"); } })(); } catch { /* */ } }, [startBgPolling]); /* ══════════════════════════════════════════════════════════ */ /* Background sync — fire-and-forget server-side processing */ /* ══════════════════════════════════════════════════════════ */ const handleSyncBackground = useCallback( async (mode: "base" | "magic") => { if (!siruta || exporting) return; try { const res = await fetch("/api/eterra/sync-background", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ siruta, mode, includeNoGeometry: includeNoGeom, }), }); const data = (await res.json()) as { jobId?: string; error?: string }; if (!res.ok || data.error) { // Transient error — ignored in the extracted component // (parent's syncProgress state is not available here) console.warn("[sync-background]", data.error ?? `Eroare ${res.status}`); return; } const jid = data.jobId!; setBgJobId(jid); setBgProgress({ jobId: jid, downloaded: 0, total: 100, status: "running", phase: "Pornire sincronizare fundal", }); setBgPhaseTrail(["Pornire sincronizare fundal"]); // Persist in localStorage so we can recover on page refresh try { localStorage.setItem( "parcel-sync:bg-job", JSON.stringify({ jobId: jid, siruta, startedAt: new Date().toISOString(), }), ); } catch { /* */ } startBgPolling(jid); } catch (error) { const msg = error instanceof Error ? error.message : "Eroare rețea"; console.warn("[sync-background]", msg); } }, [siruta, exporting, includeNoGeom, startBgPolling], ); /* ══════════════════════════════════════════════════════════ */ /* Download from DB */ /* ══════════════════════════════════════════════════════════ */ const handleDownloadFromDb = useCallback( async (mode: "base" | "magic") => { if (!siruta || downloadingFromDb) return; setDownloadingFromDb(true); try { const res = await fetch("/api/eterra/export-local", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ siruta, mode }), }); if (!res.ok) { const err = (await res.json().catch(() => ({}))) as { error?: string; }; throw new Error(err.error ?? `HTTP ${res.status}`); } const blob = await res.blob(); const cd = res.headers.get("Content-Disposition") ?? ""; const match = /filename="?([^"]+)"?/.exec(cd); const filename = match?.[1] ?? `eterra_uat_${siruta}_${mode}_local.zip`; const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch (error) { const msg = error instanceof Error ? error.message : "Eroare descărcare"; console.warn("[download-from-db]", msg); } setDownloadingFromDb(false); }, [siruta, downloadingFromDb], ); /* ══════════════════════════════════════════════════════════ */ /* Render */ /* ══════════════════════════════════════════════════════════ */ return (
{/* DB freshness status */} {sirutaValid && dbLayersSummary.length > 0 && (
{dbTotalFeatures.toLocaleString("ro-RO")} {" "} entități în DB din{" "} {dbLayersSummary.length} {" "} layere {(() => { const freshCount = dbLayersSummary.filter( (l) => l.isFresh, ).length; const staleCount = dbLayersSummary.length - freshCount; const oldestSync = dbLayersSummary.reduce( (oldest, l) => { if (!l.lastSynced) return oldest; if (!oldest || l.lastSynced < oldest) return l.lastSynced; return oldest; }, null as Date | null, ); return ( <> {staleCount === 0 ? ( Proaspete ) : ( {staleCount} vechi )} {oldestSync && ( Ultima sincronizare: {relativeTime(oldestSync)} )} ); })()}
)} {/* Hero buttons */} {sirutaValid && session.connected ? (
) : ( {!session.connected ? ( <>

Conectează-te la eTerra pentru a activa exportul.

) : ( <>

Selectează un UAT pentru a activa exportul.

)}
)} {/* No-geometry option — shown after auto-scan completes */} {sirutaValid && session.connected && (() => { const scanDone = noGeomScan !== null && noGeomScanSiruta === siruta; const estimatedNoGeom = scanDone ? Math.max( 0, noGeomScan.totalImmovables - noGeomScan.remoteGisCount, ) : 0; const hasNoGeomParcels = scanDone && estimatedNoGeom > 0; const scanning = noGeomScanning; // Still scanning if (scanning) return (
Se scanează lista de imobile din eTerra… (max 2 min)

Poți folosi butoanele de mai jos fără să aștepți scanarea.

); // Scan timed out if (scanDone && noGeomScan.scannedAt === "timeout") return (
Scanarea a depășit 2 minute — serverul eTerra e lent.

Poți lansa sincronizarea fundal fără rezultate de scanare. Include no-geom nu va fi disponibil.

); // Helper: local DB status line const staleEnrichment = scanDone && noGeomScan.localDbEnriched > 0 && noGeomScan.localDbEnrichedComplete < noGeomScan.localDbEnriched; const staleCount = scanDone ? noGeomScan.localDbEnriched - noGeomScan.localDbEnrichedComplete : 0; const localDbLine = scanDone && noGeomScan.localDbTotal > 0 && (
Baza de date locală:{" "} {noGeomScan.localDbWithGeom.toLocaleString("ro-RO")} {" "} cu geometrie {noGeomScan.localDbNoGeom > 0 && ( <> {" + "} {noGeomScan.localDbNoGeom.toLocaleString("ro-RO")} {" "} fără geometrie )} {noGeomScan.localDbEnriched > 0 && ( <> {" · "} {noGeomScan.localDbEnriched.toLocaleString("ro-RO")} {" "} îmbogățite {staleEnrichment && ( {" "} ({staleCount.toLocaleString("ro-RO")} incomplete) )} )} {noGeomScan.localSyncFresh && ( (proaspăt) )}
{staleEnrichment && (
{staleCount.toLocaleString("ro-RO")} parcele au îmbogățire veche (lipsă PROPRIETARI_VECHI). Vor fi re-îmbogățite la următorul export Magic.
)}
); // Helper: workflow preview (what Magic will do) const workflowPreview = scanDone && (

La apăsarea Magic, pașii vor fi:

  1. {"Sync GIS — "} 0 ? "text-emerald-600 dark:text-emerald-400" : "text-foreground", )} > {noGeomScan.localSyncFresh && noGeomScan.localDbWithGeom > 0 ? "skip (date proaspete în DB)" : `descarcă ${noGeomScan.remoteGisCount.toLocaleString("ro-RO")} terenuri` + (noGeomScan.remoteCladiriCount > 0 ? ` + ${noGeomScan.remoteCladiriCount.toLocaleString("ro-RO")} clădiri` : "")}
  2. {includeNoGeom && (
  3. Import parcele fără geometrie —{" "} {(() => { const usefulNoGeom = noGeomScan.qualityBreakdown.useful; const newNoGeom = Math.max( 0, usefulNoGeom - noGeomScan.localDbNoGeom, ); const filtered = noGeomScan.qualityBreakdown.empty; return newNoGeom > 0 ? `~${newNoGeom.toLocaleString("ro-RO")} noi de importat` + (filtered > 0 ? ` (${filtered.toLocaleString("ro-RO")} filtrate)` : "") : "deja importate"; })()}
  4. )}
  5. \u00cembogățire CF, proprietari, adrese —{" "} {(() => { // What will be in DB after sync + optional no-geom import: // If DB is empty: sync will add remoteGisCount geo features // If DB is fresh: keep localDbTotal const geoAfterSync = noGeomScan.localSyncFresh && noGeomScan.localDbWithGeom > 0 ? noGeomScan.localDbWithGeom : noGeomScan.remoteGisCount; const noGeomAfterImport = includeNoGeom ? Math.max( noGeomScan.localDbNoGeom, noGeomScan.qualityBreakdown.useful, ) : noGeomScan.localDbNoGeom; const totalAfter = geoAfterSync + noGeomAfterImport; const remaining = totalAfter - noGeomScan.localDbEnrichedComplete; return remaining > 0 ? `~${remaining.toLocaleString("ro-RO")} de procesat (~${Math.ceil((remaining * 0.25) / 60)} min)` : "deja îmbogățite"; })()}
  6. Generare GPKG + CSV
  7. Comprimare ZIP + descărcare
); // No-geometry parcels found if (hasNoGeomParcels) return (

Layer GIS:{" "} {noGeomScan.remoteGisCount.toLocaleString("ro-RO")} {" "} terenuri {noGeomScan.remoteCladiriCount > 0 && ( <> {" + "} {noGeomScan.remoteCladiriCount.toLocaleString( "ro-RO", )} {" "} clădiri )} {" · "} Lista imobile:{" "} {noGeomScan.totalImmovables.toLocaleString("ro-RO")} {" (estimat "} ~ {Math.max( 0, noGeomScan.totalImmovables - noGeomScan.remoteGisCount, ).toLocaleString("ro-RO")} {" fără geometrie)"}

Cele fără geometrie există în baza de date eTerra dar nu au contur desenat în layerul GIS.

{localDbLine}
{/* Quality breakdown of no-geom items */} {scanDone && noGeomScan.noGeomCount > 0 && (

Calitate date (din{" "} {noGeomScan.noGeomCount.toLocaleString("ro-RO")} fără geometrie):

Cu nr. cadastral eTerra:{" "} {noGeomScan.qualityBreakdown.withCadRef.toLocaleString( "ro-RO", )} Cu nr. CF/LB:{" "} {noGeomScan.qualityBreakdown.withPaperLb.toLocaleString( "ro-RO", )} Cu nr. cad. pe hârtie:{" "} {noGeomScan.qualityBreakdown.withPaperCad.toLocaleString( "ro-RO", )} Cu suprafață:{" "} {noGeomScan.qualityBreakdown.withArea.toLocaleString( "ro-RO", )} Active (status=1):{" "} {noGeomScan.qualityBreakdown.withActiveStatus.toLocaleString( "ro-RO", )} Cu carte funciară:{" "} {noGeomScan.qualityBreakdown.withLandbook.toLocaleString( "ro-RO", )}
Utilizabile:{" "} {noGeomScan.qualityBreakdown.useful.toLocaleString( "ro-RO", )} {noGeomScan.qualityBreakdown.empty > 0 && ( Filtrate (fără CF/inactive/fără date):{" "} {noGeomScan.qualityBreakdown.empty.toLocaleString( "ro-RO", )} )}
)} {includeNoGeom && (

{noGeomScan.qualityBreakdown.empty > 0 ? `Din ${noGeomScan.noGeomCount.toLocaleString("ro-RO")} fără geometrie, ~${noGeomScan.qualityBreakdown.useful.toLocaleString("ro-RO")} vor fi importate (imobile electronice cu CF). ${noGeomScan.qualityBreakdown.empty.toLocaleString("ro-RO")} vor fi filtrate (fără carte funciară, inactive sau fără date).` : "Vor fi importate în DB și incluse în CSV + Magic GPKG (coloana HAS_GEOMETRY=0/1)."}{" "} \u00cen GPKG de bază apar doar cele cu geometrie.

)} {workflowPreview}
); // Scan done, all parcels have geometry (or totalImmovables=0 => workspace issue) if (scanDone && !hasNoGeomParcels) return (
{noGeomScan.totalImmovables > 0 ? ( <> Toate cele{" "} {noGeomScan.totalImmovables.toLocaleString("ro-RO")}{" "} imobile din eTerra au geometrie — nimic de importat suplimentar. {noGeomScan.localDbTotal > 0 && ( ({noGeomScan.localDbTotal.toLocaleString("ro-RO")}{" "} în DB local {noGeomScan.localDbEnriched > 0 && `, ${noGeomScan.localDbEnriched.toLocaleString("ro-RO")} îmbogățite`} {noGeomScan.localDbEnriched > 0 && noGeomScan.localDbEnrichedComplete < noGeomScan.localDbEnriched && ( {` (${(noGeomScan.localDbEnriched - noGeomScan.localDbEnrichedComplete).toLocaleString("ro-RO")} incomplete)`} )} {noGeomScan.localSyncFresh && ", proaspăt"}) )} ) : ( <> Nu s-au găsit imobile în lista eTerra pentru acest UAT. Verifică sesiunea eTerra. )}
); return null; })()} {/* ── Background sync + Download from DB ──────────────── */} {sirutaValid && ( {/* Row 1: Section label */}
Procesare fundal & descărcare din DB — pornește sincronizarea, închide pagina, descarcă mai târziu
{/* Include no-geom toggle (works independently of scan) */} {session.connected && ( )} {/* Row 2: Background sync buttons */} {session.connected && (
)} {/* Row 3: Download from DB buttons */} {dbTotalFeatures > 0 && (
)} {!session.connected && dbTotalFeatures === 0 && (

Conectează-te la eTerra pentru a porni sincronizarea fundal, sau sincronizează mai întâi date în baza locală.

)}
)} {/* Background sync progress */} {bgJobId && bgProgress && bgProgress.status !== "unknown" && ( {/* Label */}
Sincronizare fundal {bgProgress.status === "running" && ( (poți închide pagina) )}
{/* Phase trail */}
{bgPhaseTrail.map((p, i) => ( {i > 0 && } {p} ))}
{/* Progress info */}
{bgProgress.status === "running" && ( )} {bgProgress.status === "done" && ( )} {bgProgress.status === "error" && ( )}

{bgProgress.phase}

{bgProgress.note && (

{bgProgress.note}

)} {bgProgress.message && (

{bgProgress.message}

)}
{bgProgress.total && bgProgress.total > 0 ? Math.round( (bgProgress.downloaded / bgProgress.total) * 100, ) : 0} %
{/* Bar */}
0 ? Math.round((bgProgress.downloaded / bgProgress.total) * 100) : 0)}%`, }} />
{/* Done — show download from DB button */} {bgProgress.status === "done" && (
)} )} {/* Progress bar for bundle export */} {exportProgress && exportProgress.status !== "unknown" && exportJobId && ( {/* Phase trail */}
{phaseTrail.map((p, i) => ( {i > 0 && } {p} ))}
{/* Progress info */}
{exportProgress.status === "running" && ( )} {exportProgress.status === "done" && ( )} {exportProgress.status === "error" && ( )}

{exportProgress.phase} {exportProgress.phaseCurrent != null && exportProgress.phaseTotal ? ` — ${exportProgress.phaseCurrent} / ${exportProgress.phaseTotal}` : ""}

{exportProgress.note && (

{exportProgress.note}

)} {exportProgress.message && (

{exportProgress.message}

)}
{progressPct}%
{/* Bar */}
)}
); }