From 24b565f5eaf8f838255401cce2e9a261839d7940 Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Thu, 26 Mar 2026 22:24:35 +0200 Subject: [PATCH] feat(parcel-sync): DXF export in ZIP + detailed tooltips on hero buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DXF Export: - Add gpkgToDxf() helper using ogr2ogr -f DXF (non-fatal fallback) - export-local: terenuri.dxf, cladiri.dxf, terenuri_magic.dxf in ZIP - export-bundle: same DXF files alongside GPKGs - Zero overhead — conversion runs locally from DB data, no eTerra calls Hero Button Tooltips: - Hover shows ZIP contents: layer names, entity counts, sync dates - Base tooltip: "GPKG + DXF per layer" - Magic tooltip: "GPKG + DXF + CSV complet + Raport calitate" Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/eterra/export-bundle/route.ts | 22 +++ src/app/api/eterra/export-local/route.ts | 11 ++ .../components/tabs/export-tab.tsx | 146 +++++++++++------- .../parcel-sync/services/gpkg-export.ts | 30 ++++ 4 files changed, 152 insertions(+), 57 deletions(-) diff --git a/src/app/api/eterra/export-bundle/route.ts b/src/app/api/eterra/export-bundle/route.ts index 4a4be28..a024ab5 100644 --- a/src/app/api/eterra/export-bundle/route.ts +++ b/src/app/api/eterra/export-bundle/route.ts @@ -562,6 +562,19 @@ export async function POST(req: Request) { zip.file("terenuri.gpkg", terenuriGpkg); zip.file("cladiri.gpkg", cladiriGpkg); + // DXF versions (non-fatal) + try { + const { gpkgToDxf } = await import( + "@/modules/parcel-sync/services/gpkg-export" + ); + const tDxf = await gpkgToDxf(terenuriGpkg, "TERENURI_ACTIVE"); + if (tDxf) zip.file("terenuri.dxf", tDxf); + const cDxf = await gpkgToDxf(cladiriGpkg, "CLADIRI_ACTIVE"); + if (cDxf) zip.file("cladiri.dxf", cDxf); + } catch { + // DXF conversion not available — skip silently + } + // ── Comprehensive quality analysis ── const withGeomRecords = dbTerenuri.filter( (r) => @@ -685,6 +698,15 @@ export async function POST(req: Request) { if (validated.mode === "magic" && magicGpkg && csvContent) { zip.file("terenuri_magic.gpkg", magicGpkg); + try { + const { gpkgToDxf } = await import( + "@/modules/parcel-sync/services/gpkg-export" + ); + const mDxf = await gpkgToDxf(magicGpkg, "TERENURI_MAGIC"); + if (mDxf) zip.file("terenuri_magic.dxf", mDxf); + } catch { + // DXF conversion not available + } zip.file("terenuri_complet.csv", csvContent); report.magic = { csvRows: csvContent.split("\n").length - 1, diff --git a/src/app/api/eterra/export-local/route.ts b/src/app/api/eterra/export-local/route.ts index 187bd50..c8757cb 100644 --- a/src/app/api/eterra/export-local/route.ts +++ b/src/app/api/eterra/export-local/route.ts @@ -182,6 +182,15 @@ async function buildFullZip(siruta: string, mode: "base" | "magic") { zip.file("terenuri.gpkg", terenuriGpkg); zip.file("cladiri.gpkg", cladiriGpkg); + // DXF versions (non-fatal — ogr2ogr may not be available) + const { gpkgToDxf } = await import( + "@/modules/parcel-sync/services/gpkg-export" + ); + const terenuriDxf = await gpkgToDxf(terenuriGpkg, "TERENURI_ACTIVE"); + if (terenuriDxf) zip.file("terenuri.dxf", terenuriDxf); + const cladiriDxf = await gpkgToDxf(cladiriGpkg, "CLADIRI_ACTIVE"); + if (cladiriDxf) zip.file("cladiri.dxf", cladiriDxf); + if (mode === "magic") { // ── Magic: enrichment-merged GPKG + CSV + quality report ── const headers = [ @@ -295,6 +304,8 @@ async function buildFullZip(siruta: string, mode: "base" | "magic") { }); zip.file("terenuri_magic.gpkg", magicGpkg); + const magicDxf = await gpkgToDxf(magicGpkg, "TERENURI_MAGIC"); + if (magicDxf) zip.file("terenuri_magic.dxf", magicDxf); zip.file("terenuri_complet.csv", csvRows.join("\n")); // ── Quality analysis ── diff --git a/src/modules/parcel-sync/components/tabs/export-tab.tsx b/src/modules/parcel-sync/components/tabs/export-tab.tsx index 7cb6304..179cee0 100644 --- a/src/modules/parcel-sync/components/tabs/export-tab.tsx +++ b/src/modules/parcel-sync/components/tabs/export-tab.tsx @@ -680,65 +680,97 @@ export function ExportTab({ {/* Hero buttons */} {sirutaValid && (session.connected || canExportLocal) ? (
-
- + {(() => { + // Build tooltip with layer details for hero buttons + const layerLines = dbLayersSummary + .filter((l) => l.count > 0) + .sort((a, b) => b.count - a.count) + .map( + (l) => + `${l.label}: ${l.count.toLocaleString("ro")} entitati${l.lastSynced ? ` (sync ${relativeTime(l.lastSynced)})` : ""}`, + ); + const enriched = dbLayersSummary.reduce( + (sum, l) => { + const enrichCount = + syncRuns.find( + (r) => r.layerId === l.id && r.status === "done", + )?.totalLocal ?? 0; + return sum + enrichCount; + }, + 0, + ); + const baseTooltip = layerLines.length > 0 + ? `ZIP contine:\n• ${layerLines.join("\n• ")}\n\nFormate: GPKG + DXF per layer` + : "Nicio data in DB"; + const magicTooltip = layerLines.length > 0 + ? `ZIP contine:\n• ${layerLines.join("\n• ")}\n\nFormate: GPKG + DXF + CSV complet\n+ Raport calitate enrichment` + : "Nicio data in DB"; - + +
- -
+ ); + })()} + {canExportLocal && session.connected && (