From 129b62758cb7dc93f5dcadcaa517bc5831b9813c Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Fri, 6 Mar 2026 18:41:11 +0200 Subject: [PATCH] refactor(parcel-sync): global UAT bar, connection pill, reorder tabs - UAT autocomplete always visible above tabs (all tabs share it) - Connection status pill in top-right: breathing green dot when connected, dropdown with credentials form / disconnect button - Tab order: Cautare Parcele (1st) -> Catalog Layere -> Export (last) - Renamed 'Butonul Magic' to just 'Magic' - Removed connection/UAT cards from inside Export tab --- src/app/api/eterra/export-bundle/route.ts | 106 +- src/app/api/eterra/export-layer-gpkg/route.ts | 28 +- .../components/parcel-sync-module.tsx | 1029 +++++++++-------- 3 files changed, 613 insertions(+), 550 deletions(-) diff --git a/src/app/api/eterra/export-bundle/route.ts b/src/app/api/eterra/export-bundle/route.ts index 366daeb..633f0ee 100644 --- a/src/app/api/eterra/export-bundle/route.ts +++ b/src/app/api/eterra/export-bundle/route.ts @@ -35,8 +35,7 @@ const validate = (body: ExportBundleRequest) => { return { username, password, siruta, jobId, mode }; }; -const sleep = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const scheduleClear = (jobId?: string) => { if (!jobId) return; @@ -112,11 +111,7 @@ export async function POST(req: Request) { pushProgress(); }; - const setPhaseState = ( - next: string, - weight: number, - nextTotal?: number, - ) => { + const setPhaseState = (next: string, weight: number, nextTotal?: number) => { phase = next; currentWeight = weight; phaseTotal = nextTotal; @@ -145,7 +140,7 @@ export async function POST(req: Request) { updateOverall(0); }; - const withHeartbeat = async (task: () => Promise) => { + const withHeartbeat = async (task: () => Promise) => { let tick = 0.1; updatePhaseProgress(tick, 1); const interval = setInterval(() => { @@ -284,7 +279,7 @@ export async function POST(req: Request) { let lastRequest = 0; const minInterval = 250; - const throttled = async (fn: () => Promise) => { + const throttled = async (fn: () => Promise) => { let attempt = 0; while (true) { const now = Date.now(); @@ -330,7 +325,11 @@ export async function POST(req: Request) { const normalizeIntravilan = (values: string[]) => { const normalized = values - .map((v) => String(v ?? "").trim().toLowerCase()) + .map((v) => + String(v ?? "") + .trim() + .toLowerCase(), + ) .filter(Boolean); const unique = new Set(normalized); if (!unique.size) return "-"; @@ -359,8 +358,7 @@ export async function POST(req: Request) { const address = item?.immovableAddresses?.[0]?.address ?? null; if (!address) return "-"; const parts: string[] = []; - if (address.addressDescription) - parts.push(address.addressDescription); + if (address.addressDescription) parts.push(address.addressDescription); if (address.street) parts.push(`Str. ${address.street}`); if (address.buildingNo) parts.push(`Nr. ${address.buildingNo}`); if (address.locality?.name) parts.push(address.locality.name); @@ -368,18 +366,12 @@ export async function POST(req: Request) { }; /* Building cross-ref map */ - const buildingMap = new Map< - string, - { has: boolean; legal: boolean } - >(); + const buildingMap = new Map(); for (const feature of cladiriFeatures) { const attrs = feature.attributes ?? {}; - const immovableId = - attrs.IMMOVABLE_ID ?? attrs.IMOVABLE_ID ?? null; + const immovableId = attrs.IMMOVABLE_ID ?? attrs.IMOVABLE_ID ?? null; const workspaceId = attrs.WORKSPACE_ID ?? null; - const baseRef = baseCadRef( - attrs.NATIONAL_CADASTRAL_REFERENCE ?? "", - ); + const baseRef = baseCadRef(attrs.NATIONAL_CADASTRAL_REFERENCE ?? ""); const isLegal = Number(attrs.IS_LEGAL ?? 0) === 1 || String(attrs.IS_LEGAL ?? "").toLowerCase() === "true"; @@ -408,8 +400,7 @@ export async function POST(req: Request) { const addOwner = (landbook: string, name: string) => { if (!landbook || !name) return; - const existing = - ownersByLandbook.get(landbook) ?? new Set(); + const existing = ownersByLandbook.get(landbook) ?? new Set(); existing.add(name); ownersByLandbook.set(landbook, existing); }; @@ -444,9 +435,7 @@ export async function POST(req: Request) { (listResponse?.content ?? []).forEach((item: any) => { const idKey = normalizeId(item?.immovablePk); if (idKey) immovableListById.set(idKey, item); - const cadKey = normalizeCadRef( - item?.identifierDetails ?? "", - ); + const cadKey = normalizeCadRef(item?.identifierDetails ?? ""); if (cadKey) immovableListByCad.set(cadKey, item); }); listPage += 1; @@ -499,10 +488,7 @@ export async function POST(req: Request) { ]; csvRows.push(headers.join(",")); - const detailsByObjectId = new Map< - string, - Record - >(); + const detailsByObjectId = new Map>(); for (let index = 0; index < terenuriFeatures.length; index += 1) { const feature = terenuriFeatures[index]!; @@ -533,15 +519,11 @@ export async function POST(req: Request) { ); immAppsCache.set(appKey, apps); } - const chosen = pickApplication( - apps, - Number(applicationId ?? 0), - ); + const chosen = pickApplication(apps, Number(applicationId ?? 0)); const appId = chosen?.applicationId ?? (applicationId ? Number(applicationId) : null); - solicitant = - chosen?.solicitant ?? chosen?.deponent ?? solicitant; + solicitant = chosen?.solicitant ?? chosen?.deponent ?? solicitant; if (appId) { const folKey = `${workspaceId}:${immovableId}:${appId}`; @@ -563,8 +545,7 @@ export async function POST(req: Request) { } } - const cadRefRaw = (attrs.NATIONAL_CADASTRAL_REFERENCE ?? - "") as string; + const cadRefRaw = (attrs.NATIONAL_CADASTRAL_REFERENCE ?? "") as string; const cadRef = normalizeCadRef(cadRefRaw); const immKey = normalizeId(immovableId); const listItem = @@ -573,51 +554,37 @@ export async function POST(req: Request) { const docKey = listItem?.immovablePk ? normalizeId(listItem.immovablePk) : ""; - const docItem = docKey - ? docByImmovable.get(docKey) - : undefined; + const docItem = docKey ? docByImmovable.get(docKey) : undefined; const landbookIE = docItem?.landbookIE ?? ""; const owners = landbookIE && ownersByLandbook.get(String(landbookIE)) - ? Array.from( - ownersByLandbook.get(String(landbookIE)) ?? [], - ) + ? Array.from(ownersByLandbook.get(String(landbookIE)) ?? []) : []; const ownersByCad = cadRefRaw && ownersByLandbook.get(String(cadRefRaw)) - ? Array.from( - ownersByLandbook.get(String(cadRefRaw)) ?? [], - ) + ? Array.from(ownersByLandbook.get(String(cadRefRaw)) ?? []) : []; proprietari = - Array.from(new Set([...owners, ...ownersByCad])).join( - "; ", - ) || proprietari; + Array.from(new Set([...owners, ...ownersByCad])).join("; ") || + proprietari; nrCF = docItem?.landbookIE || listItem?.paperLbNo || listItem?.paperCadNo || nrCF; - const nrCFVechiRaw = - listItem?.paperLbNo || listItem?.paperCadNo || ""; + const nrCFVechiRaw = listItem?.paperLbNo || listItem?.paperCadNo || ""; nrCFVechi = docItem?.landbookIE && nrCFVechiRaw !== nrCF ? nrCFVechiRaw : nrCFVechi; nrTopo = - listItem?.topNo || - docItem?.topNo || - listItem?.paperCadNo || - nrTopo; - addressText = listItem - ? formatAddress(listItem) - : addressText; + listItem?.topNo || docItem?.topNo || listItem?.paperCadNo || nrTopo; + addressText = listItem ? formatAddress(listItem) : addressText; const parcelRef = baseCadRef(cadRefRaw); const wKey = makeWorkspaceKey(workspaceId, immovableId); - const build = - (immKey ? buildingMap.get(immKey) : undefined) ?? + const build = (immKey ? buildingMap.get(immKey) : undefined) ?? (wKey ? buildingMap.get(wKey) : undefined) ?? (parcelRef ? buildingMap.get(parcelRef) : undefined) ?? { has: false, @@ -637,10 +604,8 @@ export async function POST(req: Request) { NR_TOPO: nrTopo, ADRESA: addressText, PROPRIETARI: proprietari, - SUPRAFATA_2D: - areaValue !== null ? Number(areaValue.toFixed(2)) : "", - SUPRAFATA_R: - areaValue !== null ? Math.round(areaValue) : "", + SUPRAFATA_2D: areaValue !== null ? Number(areaValue.toFixed(2)) : "", + SUPRAFATA_R: areaValue !== null ? Math.round(areaValue) : "", SOLICITANT: solicitant, INTRAVILAN: intravilan, CATEGORIE_FOLOSINTA: categorie, @@ -671,8 +636,7 @@ export async function POST(req: Request) { ]; csvRows.push(row.join(",")); - if (index % 10 === 0) - updatePhaseProgress(index + 1, terenuriCount); + if (index % 10 === 0) updatePhaseProgress(index + 1, terenuriCount); } updatePhaseProgress(terenuriFeatures.length, terenuriCount); @@ -737,9 +701,7 @@ export async function POST(req: Request) { ]), ); const magicFeatures = terenuriGeo.features.map((feature) => { - const objectId = String( - feature.properties?.OBJECTID ?? "", - ); + const objectId = String(feature.properties?.OBJECTID ?? ""); const extra = detailsByObjectId.get(objectId) ?? {}; return { ...feature, @@ -870,9 +832,7 @@ export async function POST(req: Request) { scheduleClear(jobId); const lower = errMessage.toLowerCase(); const statusCode = - lower.includes("login failed") || lower.includes("session") - ? 401 - : 400; + lower.includes("login failed") || lower.includes("session") ? 401 : 400; return new Response(JSON.stringify({ error: errMessage }), { status: statusCode, headers: { "Content-Type": "application/json" }, diff --git a/src/app/api/eterra/export-layer-gpkg/route.ts b/src/app/api/eterra/export-layer-gpkg/route.ts index 97b5c46..b275449 100644 --- a/src/app/api/eterra/export-layer-gpkg/route.ts +++ b/src/app/api/eterra/export-layer-gpkg/route.ts @@ -75,11 +75,7 @@ export async function POST(req: Request) { pushProgress(); }; - const setPhaseState = ( - next: string, - weight: number, - nextTotal?: number, - ) => { + const setPhaseState = (next: string, weight: number, nextTotal?: number) => { phase = next; currentWeight = weight; phaseTotal = nextTotal; @@ -108,7 +104,7 @@ export async function POST(req: Request) { updateOverall(0); }; - const withHeartbeat = async (task: () => Promise) => { + const withHeartbeat = async (task: () => Promise) => { let tick = 0.1; updatePhaseProgress(tick, 1); const interval = setInterval(() => { @@ -161,8 +157,7 @@ export async function POST(req: Request) { count = await client.countLayer(layer, validated.siruta); } } catch (error) { - const msg = - error instanceof Error ? error.message : "Count error"; + const msg = error instanceof Error ? error.message : "Count error"; if (!msg.toLowerCase().includes("count unavailable")) throw error; } updatePhaseProgress(2, 2); @@ -187,9 +182,7 @@ export async function POST(req: Request) { onProgress: (value, totalCount) => { updatePhaseProgress( value, - typeof totalCount === "number" - ? totalCount - : value + pageSize, + typeof totalCount === "number" ? totalCount : value + pageSize, ); }, }) @@ -200,16 +193,11 @@ export async function POST(req: Request) { onProgress: (value, totalCount) => { updatePhaseProgress( value, - typeof totalCount === "number" - ? totalCount - : value + pageSize, + typeof totalCount === "number" ? totalCount : value + pageSize, ); }, }); - updatePhaseProgress( - features.length, - count ?? features.length, - ); + updatePhaseProgress(features.length, count ?? features.length); finishPhase(); /* Fields */ @@ -274,9 +262,7 @@ export async function POST(req: Request) { scheduleClear(jobId); const lower = errMessage.toLowerCase(); const statusCode = - lower.includes("login failed") || lower.includes("session") - ? 401 - : 400; + lower.includes("login failed") || lower.includes("session") ? 401 : 400; return new Response(JSON.stringify({ error: errMessage }), { status: statusCode, headers: { "Content-Type": "application/json" }, diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index 7895598..6df49ab 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -16,7 +16,9 @@ import { ChevronDown, ChevronUp, FileDown, - Plug, + LogOut, + Wifi, + WifiOff, } from "lucide-react"; import { Button } from "@/shared/components/ui/button"; import { Input } from "@/shared/components/ui/input"; @@ -25,8 +27,6 @@ import { Badge } from "@/shared/components/ui/badge"; import { Card, CardContent, - CardHeader, - CardTitle, } from "@/shared/components/ui/card"; import { Tabs, @@ -34,6 +34,13 @@ import { TabsList, TabsTrigger, } from "@/shared/components/ui/tabs"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/shared/components/ui/dropdown-menu"; import { cn } from "@/shared/lib/utils"; import { LAYER_CATALOG, @@ -88,6 +95,180 @@ function formatArea(val?: number | null) { return val.toLocaleString("ro-RO", { maximumFractionDigits: 2 }) + " mp"; } +/* ------------------------------------------------------------------ */ +/* Connection Status Pill */ +/* ------------------------------------------------------------------ */ + +function ConnectionPill({ + connected, + connecting, + connectionError, + connectedAt, + username, + password, + onUsernameChange, + onPasswordChange, + onConnect, + onDisconnect, +}: { + connected: boolean; + connecting: boolean; + connectionError: string; + connectedAt: Date | null; + username: string; + password: string; + onUsernameChange: (v: string) => void; + onPasswordChange: (v: string) => void; + onConnect: () => void; + onDisconnect: () => void; +}) { + const elapsed = connectedAt + ? Math.floor((Date.now() - connectedAt.getTime()) / 60_000) + : 0; + const elapsedLabel = + elapsed < 1 + ? "acum" + : elapsed < 60 + ? `${elapsed} min` + : `${Math.floor(elapsed / 60)}h ${elapsed % 60}m`; + + return ( + + + + + + + {/* Status header */} +
+
+ + Conexiune eTerra + + {connected && ( + + {elapsedLabel} + + )} +
+ {connected && username && ( +

+ {username} +

+ )} + {connectionError && ( +

{connectionError}

+ )} +
+ + {/* Credentials form */} + {!connected && ( +
+
+ + onUsernameChange(e.target.value)} + autoComplete="username" + className="h-8 text-xs" + /> +
+
+ + onPasswordChange(e.target.value)} + autoComplete="current-password" + className="h-8 text-xs" + /> +
+ +
+ )} + + {/* Connected actions */} + {connected && ( + <> + +
+ +
+ + )} +
+
+ ); +} + /* ------------------------------------------------------------------ */ /* Main Component */ /* ------------------------------------------------------------------ */ @@ -97,6 +278,7 @@ export function ParcelSyncModule() { const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); const [connectionError, setConnectionError] = useState(""); + const [connectedAt, setConnectedAt] = useState(null); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -185,14 +367,24 @@ export function ParcelSyncModule() { success?: boolean; error?: string; }; - if (data.success) setConnected(true); - else setConnectionError(data.error ?? "Eroare conectare"); + if (data.success) { + setConnected(true); + setConnectedAt(new Date()); + } else { + setConnectionError(data.error ?? "Eroare conectare"); + } } catch { setConnectionError("Eroare rețea"); } setConnecting(false); }, [username, password]); + const handleDisconnect = useCallback(() => { + setConnected(false); + setConnectedAt(null); + setConnectionError(""); + }, []); + /* ════════════════════════════════════════════════════════════ */ /* Progress polling */ /* ════════════════════════════════════════════════════════════ */ @@ -283,8 +475,7 @@ export function ParcelSyncModule() { a.remove(); URL.revokeObjectURL(url); } catch (error) { - const msg = - error instanceof Error ? error.message : "Eroare export"; + const msg = error instanceof Error ? error.message : "Eroare export"; setExportProgress((prev) => prev ? { ...prev, status: "error", message: msg } @@ -344,8 +535,7 @@ export function ParcelSyncModule() { const blob = await res.blob(); const cd = res.headers.get("Content-Disposition") ?? ""; const match = /filename="?([^"]+)"?/.exec(cd); - const filename = - match?.[1] ?? `eterra_uat_${siruta}_${layerId}.gpkg`; + const filename = match?.[1] ?? `eterra_uat_${siruta}_${layerId}.gpkg`; const url = URL.createObjectURL(blob); const a = document.createElement("a"); @@ -356,8 +546,7 @@ export function ParcelSyncModule() { a.remove(); URL.revokeObjectURL(url); } catch (error) { - const msg = - error instanceof Error ? error.message : "Eroare export"; + const msg = error instanceof Error ? error.message : "Eroare export"; setExportProgress((prev) => prev ? { ...prev, status: "error", message: msg } @@ -432,9 +621,7 @@ export function ParcelSyncModule() { const progressPct = exportProgress?.total && exportProgress.total > 0 - ? Math.round( - (exportProgress.downloaded / exportProgress.total) * 100, - ) + ? Math.round((exportProgress.downloaded / exportProgress.total) * 100) : 0; /* ════════════════════════════════════════════════════════════ */ @@ -442,112 +629,18 @@ export function ParcelSyncModule() { /* ════════════════════════════════════════════════════════════ */ return ( - - - - - Export - - - - Catalog Layere - - - - Căutare Parcele - - - - {/* ═══════════════════════════════════════════════════════ */} - {/* Tab 1: Export (connection + UAT + hero buttons) */} - {/* ═══════════════════════════════════════════════════════ */} - - {/* Connection card */} - - - - - Conexiune eTerra - - - - {/* Credentials row (optional, env fallback) */} -
-
- - setUsername(e.target.value)} - autoComplete="username" - /> -
-
- - setPassword(e.target.value)} - autoComplete="current-password" - /> -
-
- -
-
- - {/* Status */} -
- {connected && ( - - - Conectat - - )} - {connectionError && ( - - - {connectionError} - - )} -
-
-
- - {/* UAT selector card */} - - - - - Selectare UAT - - - -
- + + {/* ═══════════════════════ Persistent header ═══════════════ */} +
+ {/* UAT + Connection row */} +
+ {/* UAT autocomplete — always visible */} +
+
+ { setUatQuery(e.target.value); @@ -555,359 +648,94 @@ export function ParcelSyncModule() { }} onFocus={() => setShowUatResults(true)} onBlur={() => setTimeout(() => setShowUatResults(false), 150)} - className="font-medium" + className="pl-9 font-medium" autoComplete="off" /> - - {/* Dropdown */} - {showUatResults && uatResults.length > 0 && ( -
- {uatResults.map((item) => ( - - ))} -
- )} - - {/* Selected indicator */} - {sirutaValid && ( -

- UAT selectat:{" "} - - {siruta} - -

- )}
- - - {/* Hero buttons */} - {sirutaValid && connected && ( -
- + )} - -
- )} - - {/* Not connected / no UAT hints */} - {!connected && sirutaValid && ( - - - -

Conectează-te la eTerra pentru a activa exportul.

-
-
- )} - {connected && !sirutaValid && ( - - - -

Selectează un UAT pentru a activa exportul.

-
-
- )} - - {/* Progress bar */} - {exportProgress && - exportProgress.status !== "unknown" && - exportJobId && ( - - - {/* Phase trail */} -
- {phaseTrail.map((p, i) => ( - - {i > 0 && } - - {p} - + {/* Dropdown */} + {showUatResults && uatResults.length > 0 && ( +
+ {uatResults.map((item) => ( +
- - {/* 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 */} -
-
-
- - - )} - - - {/* ═══════════════════════════════════════════════════════ */} - {/* Tab 2: Layer catalog */} - {/* ═══════════════════════════════════════════════════════ */} - - {!sirutaValid || !connected ? ( - - - -

- {!connected - ? "Conectează-te la eTerra și selectează un UAT." - : "Selectează un UAT pentru a vedea catalogul de layere."} -

-
-
- ) : ( -
- {(Object.keys(LAYER_CATEGORY_LABELS) as LayerCategory[]).map( - (cat) => { - const layers = layersByCategory[cat]; - if (!layers?.length) return null; - const isExpanded = expandedCategories[cat] ?? false; - - return ( - - - - {isExpanded && ( - - {layers.map((layer) => { - const isDownloading = - downloadingLayer === layer.id; - return ( -
-
-

- {layer.label} -

-

- {layer.id} -

-
- -
- ); - })} -
- )} -
- ); - }, + + ))} +
)}
- )} - {/* Progress bar for layer download */} - {downloadingLayer && exportProgress && ( - - -
- -
-

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

-
- - {progressPct}% - -
-
-
-
- - - )} - + {/* Connection pill */} + +
+ + {/* Tab bar */} + + + + Căutare Parcele + + + + Catalog Layere + + + + Export + + +
{/* ═══════════════════════════════════════════════════════ */} - {/* Tab 3: Parcel search */} + {/* Tab 1: Parcel search */} {/* ═══════════════════════════════════════════════════════ */} {!sirutaValid ? ( -

- Selectează un UAT în tabul Export pentru a căuta parcele - sincronizate. -

+

Selectează un UAT mai sus pentru a căuta parcele.

) : ( @@ -1007,9 +835,8 @@ export function ParcelSyncModule() { ) : ( features.map((f) => { const layerLabel = - LAYER_CATALOG.find( - (l) => l.id === f.layerId, - )?.label ?? f.layerId; + LAYER_CATALOG.find((l) => l.id === f.layerId) + ?.label ?? f.layerId; return ( )}
+ + {/* ═══════════════════════════════════════════════════════ */} + {/* Tab 2: Layer catalog */} + {/* ═══════════════════════════════════════════════════════ */} + + {!sirutaValid || !connected ? ( + + + +

+ {!connected + ? "Conectează-te la eTerra și selectează un UAT." + : "Selectează un UAT pentru a vedea catalogul de layere."} +

+
+
+ ) : ( +
+ {(Object.keys(LAYER_CATEGORY_LABELS) as LayerCategory[]).map( + (cat) => { + const layers = layersByCategory[cat]; + if (!layers?.length) return null; + const isExpanded = expandedCategories[cat] ?? false; + + return ( + + + + {isExpanded && ( + + {layers.map((layer) => { + const isDownloading = downloadingLayer === layer.id; + return ( +
+
+

+ {layer.label} +

+

+ {layer.id} +

+
+ +
+ ); + })} +
+ )} +
+ ); + }, + )} +
+ )} + + {/* Progress bar for layer download */} + {downloadingLayer && exportProgress && ( + + +
+ +
+

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

+
+ + {progressPct}% + +
+
+
+
+ + + )} + + + {/* ═══════════════════════════════════════════════════════ */} + {/* Tab 3: Export */} + {/* ═══════════════════════════════════════════════════════ */} + + {/* Hero buttons */} + {sirutaValid && connected ? ( +
+ + + +
+ ) : ( + + + {!connected ? ( + <> + +

Conectează-te la eTerra pentru a activa exportul.

+ + ) : ( + <> + +

Selectează un UAT pentru a activa exportul.

+ + )} +
+
+ )} + + {/* Progress bar */} + {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 */} +
+
+
+ + + )} + ); }