feat(parcel-sync): DXF export in ZIP + detailed tooltips on hero buttons

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) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-26 22:24:35 +02:00
parent bde25d8d84
commit 24b565f5ea
4 changed files with 152 additions and 57 deletions
@@ -680,65 +680,97 @@ export function ExportTab({
{/* Hero buttons */}
{sirutaValid && (session.connected || canExportLocal) ? (
<div className="space-y-2">
<div className="grid gap-3 sm:grid-cols-2">
<Button
size="lg"
className="h-auto py-4 text-base bg-zinc-900 hover:bg-zinc-800 text-white dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
disabled={exporting || downloadingFromDb}
onClick={() =>
canExportLocal
? void handleDownloadFromDb("base")
: void handleExportBundle("base")
}
>
{(exporting || downloadingFromDb) &&
exportProgress?.phase !== "Detalii parcele" ? (
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : canExportLocal ? (
<Database className="mr-2 h-5 w-5" />
) : (
<FileDown className="mr-2 h-5 w-5" />
)}
<div className="text-left">
<div className="font-semibold">
Descarcă Terenuri și Clădiri
</div>
<div className="text-xs opacity-70 font-normal">
{canExportLocal
? `Din DB (sync ${oldestSyncDate ? relativeTime(oldestSyncDate) : "recent"})`
: hasData
? "Sync incremental + GPKG"
: "Sync complet + GPKG"}
</div>
</div>
</Button>
{(() => {
// 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";
<Button
size="lg"
className="h-auto py-4 text-base bg-teal-700 hover:bg-teal-600 text-white dark:bg-teal-600 dark:hover:bg-teal-500"
disabled={exporting || downloadingFromDb}
onClick={() =>
canExportLocal
? void handleDownloadFromDb("magic")
: void handleExportBundle("magic")
}
>
{(exporting || downloadingFromDb) &&
exportProgress?.phase === "Detalii parcele" ? (
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<Sparkles className="mr-2 h-5 w-5" />
)}
<div className="text-left">
<div className="font-semibold">Magic</div>
<div className="text-xs opacity-70 font-normal">
{canExportLocal
? `GPKG + CSV din DB (sync ${oldestSyncDate ? relativeTime(oldestSyncDate) : "recent"})`
: "Sync + îmbogățire (CF, proprietari, adresă) + GPKG + CSV"}
</div>
return (
<div className="grid gap-3 sm:grid-cols-2">
<Button
size="lg"
className="h-auto py-4 text-base bg-zinc-900 hover:bg-zinc-800 text-white dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
disabled={exporting || downloadingFromDb}
title={baseTooltip}
onClick={() =>
canExportLocal
? void handleDownloadFromDb("base")
: void handleExportBundle("base")
}
>
{(exporting || downloadingFromDb) &&
exportProgress?.phase !== "Detalii parcele" ? (
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : canExportLocal ? (
<Database className="mr-2 h-5 w-5" />
) : (
<FileDown className="mr-2 h-5 w-5" />
)}
<div className="text-left">
<div className="font-semibold">
Descarcă Terenuri și Clădiri
</div>
<div className="text-xs opacity-70 font-normal">
{canExportLocal
? `Din DB (sync ${oldestSyncDate ? relativeTime(oldestSyncDate) : "recent"})`
: hasData
? "Sync incremental + GPKG + DXF"
: "Sync complet + GPKG + DXF"}
</div>
</div>
</Button>
<Button
size="lg"
className="h-auto py-4 text-base bg-teal-700 hover:bg-teal-600 text-white dark:bg-teal-600 dark:hover:bg-teal-500"
disabled={exporting || downloadingFromDb}
title={magicTooltip}
onClick={() =>
canExportLocal
? void handleDownloadFromDb("magic")
: void handleExportBundle("magic")
}
>
{(exporting || downloadingFromDb) &&
exportProgress?.phase === "Detalii parcele" ? (
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<Sparkles className="mr-2 h-5 w-5" />
)}
<div className="text-left">
<div className="font-semibold">Magic</div>
<div className="text-xs opacity-70 font-normal">
{canExportLocal
? `GPKG + DXF + CSV din DB (sync ${oldestSyncDate ? relativeTime(oldestSyncDate) : "recent"})`
: "Sync + îmbogățire + GPKG + DXF + CSV"}
</div>
</div>
</Button>
</div>
</Button>
</div>
);
})()}
{canExportLocal && session.connected && (
<div className="text-center">
<button