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
@@ -175,3 +175,33 @@ export const buildGpkg = async (options: GpkgBuildOptions): Promise<Buffer> => {
await fs.rm(tmpDir, { recursive: true, force: true });
return buffer;
};
/**
* Convert a GPKG buffer to DXF using ogr2ogr.
* Returns null if ogr2ogr is not available or conversion fails.
*/
export const gpkgToDxf = async (
gpkgBuffer: Buffer,
layerName: string,
): Promise<Buffer | null> => {
if (!hasOgr2Ogr()) return null;
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "eterra-dxf-"));
const gpkgPath = path.join(tmpDir, "input.gpkg");
const dxfPath = path.join(tmpDir, `${layerName}.dxf`);
try {
await fs.writeFile(gpkgPath, gpkgBuffer);
await runOgr(
["-f", "DXF", dxfPath, gpkgPath, layerName],
{ ...process.env, OGR_CT_FORCE_TRADITIONAL_GIS_ORDER: "YES" },
);
const buffer = Buffer.from(await fs.readFile(dxfPath));
return buffer;
} catch {
// DXF conversion failed — not critical
return null;
} finally {
await fs.rm(tmpDir, { recursive: true, force: true });
}
};