From 3c4a75a858234ff9756c2a515fc1f6e0fd6805bd Mon Sep 17 00:00:00 2001 From: flfeders Date: Sun, 5 Jul 2026 12:31:59 +0200 Subject: [PATCH] Wire tenant archive export in frontend --- frontend/composables/useAdmin.ts | 36 ++++++++- .../pages/administration/tenants/[id].vue | 81 +++++++++++++++---- .../pages/administration/tenants/index.vue | 15 +++- 3 files changed, 108 insertions(+), 24 deletions(-) diff --git a/frontend/composables/useAdmin.ts b/frontend/composables/useAdmin.ts index 355dd85..89cc0e1 100644 --- a/frontend/composables/useAdmin.ts +++ b/frontend/composables/useAdmin.ts @@ -55,6 +55,22 @@ export type TenantImportResult = { files: { restored: number; skipped: number } } +export type TenantExportJob = { + exportId: string + tenantId?: number + status: "queued" | "running" | "ready" | "failed" | string + filename: string + fileSize?: number | null + filesDone?: number + filesTotal?: number + error?: string | null + statusUrl?: string + downloadUrl?: string | null + createdAt?: string + updatedAt?: string | null + completedAt?: string | null +} + export type SystemStatus = { checkedAt: string backend: { @@ -173,13 +189,23 @@ export const useAdmin = () => { }) } - const exportTenant = async (id: number): Promise => { - return await $api(`/api/admin/tenants/${id}/export`, { + const startTenantExport = async (id: number): Promise => { + return await $api(`/api/admin/tenants/${id}/exports`, { + method: "POST", + }) + } + + const getTenantExport = async (exportId: string): Promise => { + return await $api(`/api/admin/tenant-exports/${exportId}`) + } + + const downloadTenantExport = async (exportId: string): Promise => { + return await $api(`/api/admin/tenant-exports/${exportId}/download`, { responseType: "blob", }) } - const importTenant = async (body: Record): Promise => { + const importTenant = async (body: Record | FormData): Promise => { return await $api("/api/admin/tenant-imports", { method: "POST", body, @@ -222,7 +248,9 @@ export const useAdmin = () => { createTenant, invitePortalUser, updateTenant, - exportTenant, + startTenantExport, + getTenantExport, + downloadTenantExport, importTenant, } } diff --git a/frontend/pages/administration/tenants/[id].vue b/frontend/pages/administration/tenants/[id].vue index b541af8..87edd36 100644 --- a/frontend/pages/administration/tenants/[id].vue +++ b/frontend/pages/administration/tenants/[id].vue @@ -12,6 +12,12 @@ const loading = ref(true) const saving = ref(false) const exportingTenant = ref(false) const importingTenant = ref(false) +const tenantExportProgress = ref(null) const creatingUser = ref(false) const createUserModalOpen = ref(false) const createdUserPassword = ref("") @@ -93,26 +99,46 @@ const downloadTenantExport = async () => { if (!tenantForm.value || exportingTenant.value) return exportingTenant.value = true + tenantExportProgress.value = null try { - const blob = await admin.exportTenant(tenantForm.value.id) + const started = await admin.startTenantExport(tenantForm.value.id) + let job = started + + tenantExportProgress.value = { + status: job.status, + filesDone: job.filesDone || 0, + filesTotal: job.filesTotal || 0, + filename: job.filename, + } + + while (!["ready", "failed"].includes(job.status)) { + await new Promise((resolve) => setTimeout(resolve, 1500)) + job = await admin.getTenantExport(started.exportId) + tenantExportProgress.value = { + status: job.status, + filesDone: job.filesDone || 0, + filesTotal: job.filesTotal || 0, + filename: job.filename, + } + } + + if (job.status === "failed") { + throw new Error(job.error || "Export konnte nicht erstellt werden.") + } + + const blob = await admin.downloadTenantExport(started.exportId) const objectUrl = URL.createObjectURL(blob) const link = document.createElement("a") - const safeName = (tenantForm.value.short || tenantForm.value.name || tenantForm.value.id) - .toString() - .trim() - .replace(/[^a-z0-9_-]+/gi, "-") - .replace(/^-+|-+$/g, "") - .toLowerCase() link.href = objectUrl - link.download = `fedeo-tenant-${safeName || tenantForm.value.id}-${new Date().toISOString().slice(0, 10)}.json` + link.download = job.filename || `fedeo-tenant-${tenantForm.value.id}.fedeo-export.zip` document.body.appendChild(link) link.click() link.remove() URL.revokeObjectURL(objectUrl) - toast.add({ title: "Mandantenexport erstellt", color: "green" }) + toast.add({ title: "Mandantenexport erstellt", description: job.filename, color: "green" }) } catch (err: any) { console.error("[administration/tenants/export]", err) toast.add({ @@ -138,12 +164,22 @@ const importTenantExport = async (event: Event) => { lastImportResult.value = null try { - const rawContent = await file.text() - const exportData = JSON.parse(rawContent) - const result = await admin.importTenant({ - exportData, - targetTenantId: tenantForm.value?.id || tenantId, - }) + const targetTenantId = tenantForm.value?.id || tenantId + const isZipExport = file.name.endsWith(".zip") || file.name.endsWith(".fedeo-export.zip") || file.type === "application/zip" + let result + + if (isZipExport) { + const formData = new FormData() + formData.append("file", file) + formData.append("targetTenantId", String(targetTenantId)) + result = await admin.importTenant(formData) + } else { + result = await admin.importTenant({ + exportData: JSON.parse(await file.text()), + targetTenantId, + }) + } + const rowCount = (result.tables || []).reduce((sum, table) => sum + table.rows, 0) lastImportResult.value = { @@ -290,8 +326,19 @@ onMounted(async () => {
Full Export

- Erstellt eine portable JSON-Datei mit Mandantendaten, Benutzerzuordnungen, Rollen, Profilen und Dateiinhalten. + Erstellt ein portables ZIP-Archiv mit Mandantendaten, Benutzerzuordnungen, Rollen, Profilen und Dateien.

+
+ +

+ {{ tenantExportProgress.status === 'ready' ? 'Download wird vorbereitet' : 'Export wird erstellt' }} + + ยท {{ tenantExportProgress.filesDone }} / {{ tenantExportProgress.filesTotal }} Dateien + +

+
{ diff --git a/frontend/pages/administration/tenants/index.vue b/frontend/pages/administration/tenants/index.vue index 862c910..82f91f0 100644 --- a/frontend/pages/administration/tenants/index.vue +++ b/frontend/pages/administration/tenants/index.vue @@ -103,8 +103,17 @@ const importTenantExport = async (event: Event) => { importingTenant.value = true try { - const exportData = JSON.parse(await file.text()) - const result = await admin.importTenant(exportData) + const isZipExport = file.name.endsWith(".zip") || file.name.endsWith(".fedeo-export.zip") || file.type === "application/zip" + let result + + if (isZipExport) { + const formData = new FormData() + formData.append("file", file) + result = await admin.importTenant(formData) + } else { + result = await admin.importTenant(JSON.parse(await file.text())) + } + const rowCount = (result.tables || []).reduce((sum, table) => sum + table.rows, 0) await fetchTenants() @@ -158,7 +167,7 @@ onMounted(async () => {