KI-AGENT: Ergänze Mandantenexport und Import
This commit is contained in:
@@ -15,6 +15,8 @@ import {
|
||||
import { generateRandomPassword, hashPassword } from "../utils/password";
|
||||
import { sendMail } from "../utils/mailer";
|
||||
import { ensureTenantBaseData } from "../modules/bootstrap.service";
|
||||
import { buildTenantFullExport, importTenantFullExport } from "../utils/tenantFullExport";
|
||||
import type { TenantFullExport } from "../utils/tenantFullExport";
|
||||
|
||||
export default async function adminRoutes(server: FastifyInstance) {
|
||||
const deriveNameFromEmail = (email: string) => {
|
||||
@@ -929,6 +931,57 @@ export default async function adminRoutes(server: FastifyInstance) {
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// GET /admin/tenants/:tenant_id/export
|
||||
// -------------------------------------------------------------
|
||||
server.get("/admin/tenants/:tenant_id/export", async (req, reply) => {
|
||||
try {
|
||||
const currentUser = await requireAdmin(req, reply);
|
||||
if (!currentUser) return;
|
||||
|
||||
const { tenant_id } = req.params as { tenant_id: string };
|
||||
const tenantId = Number(tenant_id);
|
||||
if (!tenantId) {
|
||||
return reply.code(400).send({ error: "tenant_id required" });
|
||||
}
|
||||
|
||||
const exportData = await buildTenantFullExport(server, tenantId);
|
||||
const safeTenantName = String(exportData.tables.tenants?.[0]?.short || exportData.tables.tenants?.[0]?.name || tenantId)
|
||||
.replace(/[^a-z0-9_-]+/gi, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.toLowerCase();
|
||||
const filename = `fedeo-tenant-${safeTenantName || tenantId}-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
|
||||
reply.header("Content-Type", "application/json");
|
||||
reply.header("Content-Disposition", `attachment; filename="${filename}"`);
|
||||
return reply.send(exportData);
|
||||
} catch (err) {
|
||||
console.error("ERROR /admin/tenants/:tenant_id/export:", err);
|
||||
return reply.code(500).send({ error: "Internal Server Error" });
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// POST /admin/tenant-imports
|
||||
// -------------------------------------------------------------
|
||||
server.post("/admin/tenant-imports", { bodyLimit: 1024 * 1024 * 1024 }, async (req, reply) => {
|
||||
try {
|
||||
const currentUser = await requireAdmin(req, reply);
|
||||
if (!currentUser) return;
|
||||
|
||||
const exportData = req.body as TenantFullExport;
|
||||
const result = await importTenantFullExport(server, exportData);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...result,
|
||||
};
|
||||
} catch (err: any) {
|
||||
console.error("ERROR /admin/tenant-imports:", err);
|
||||
return reply.code(500).send({ error: err?.message || "Internal Server Error" });
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// PUT /admin/users/:user_id/access
|
||||
// -------------------------------------------------------------
|
||||
|
||||
293
backend/src/utils/tenantFullExport.ts
Normal file
293
backend/src/utils/tenantFullExport.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
import { FastifyInstance } from "fastify"
|
||||
import { GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3"
|
||||
|
||||
import { pool } from "../../db"
|
||||
import { s3 } from "./s3"
|
||||
import { secrets } from "./secrets"
|
||||
|
||||
type TableRows = Record<string, Record<string, any>[]>
|
||||
|
||||
export type TenantFullExport = {
|
||||
format: "fedeo.tenant-full-export"
|
||||
version: 1
|
||||
exportedAt: string
|
||||
tenantId: number
|
||||
tables: TableRows
|
||||
files: {
|
||||
id: string
|
||||
path: string
|
||||
name: string | null
|
||||
mimeType: string | null
|
||||
size: number | null
|
||||
contentBase64: string
|
||||
}[]
|
||||
}
|
||||
|
||||
type ImportResult = {
|
||||
tenantId: number
|
||||
tables: { table: string; rows: number }[]
|
||||
files: { restored: number; skipped: number }
|
||||
}
|
||||
|
||||
const quoteIdent = (value: string) => `"${value.replace(/"/g, '""')}"`
|
||||
|
||||
const tableColumns = async (client: any) => {
|
||||
const result = await client.query(`
|
||||
select table_name, column_name
|
||||
from information_schema.columns
|
||||
where table_schema = 'public'
|
||||
order by table_name, ordinal_position
|
||||
`)
|
||||
|
||||
const columnsByTable = new Map<string, string[]>()
|
||||
for (const row of result.rows) {
|
||||
const columns = columnsByTable.get(row.table_name) || []
|
||||
columns.push(row.column_name)
|
||||
columnsByTable.set(row.table_name, columns)
|
||||
}
|
||||
|
||||
return columnsByTable
|
||||
}
|
||||
|
||||
const loadRows = async (client: any, table: string, whereSql: string, params: any[] = []) => {
|
||||
const result = await client.query(`select * from ${quoteIdent(table)} where ${whereSql}`, params)
|
||||
|
||||
return result.rows
|
||||
}
|
||||
|
||||
const collectIds = (rows: Record<string, any>[], column: string) => {
|
||||
return Array.from(new Set(rows.map((row) => row[column]).filter(Boolean)))
|
||||
}
|
||||
|
||||
const addRows = (tables: TableRows, table: string, rows: Record<string, any>[]) => {
|
||||
if (!rows.length) {
|
||||
if (!tables[table]) tables[table] = []
|
||||
return
|
||||
}
|
||||
|
||||
const existingRows = tables[table] || []
|
||||
const existingKeys = new Set(existingRows.map((row) => JSON.stringify(row)))
|
||||
for (const row of rows) {
|
||||
const key = JSON.stringify(row)
|
||||
if (!existingKeys.has(key)) {
|
||||
existingRows.push(row)
|
||||
existingKeys.add(key)
|
||||
}
|
||||
}
|
||||
tables[table] = existingRows
|
||||
}
|
||||
|
||||
const loadObjectAsBase64 = async (path: string) => {
|
||||
const { Body } = await s3.send(new GetObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: path,
|
||||
}))
|
||||
|
||||
const chunks: Buffer[] = []
|
||||
for await (const chunk of Body as any) {
|
||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
||||
}
|
||||
|
||||
return Buffer.concat(chunks).toString("base64")
|
||||
}
|
||||
|
||||
export const buildTenantFullExport = async (server: FastifyInstance, tenantId: number): Promise<TenantFullExport> => {
|
||||
const client = await pool.connect()
|
||||
|
||||
try {
|
||||
const columnsByTable = await tableColumns(client)
|
||||
const tables: TableRows = {}
|
||||
|
||||
const tenantRows = await loadRows(client, "tenants", "id = $1", [tenantId])
|
||||
if (!tenantRows.length) throw new Error("Tenant nicht gefunden")
|
||||
|
||||
addRows(tables, "tenants", tenantRows)
|
||||
|
||||
for (const [table, columns] of columnsByTable.entries()) {
|
||||
if (table === "tenants") continue
|
||||
|
||||
const tenantColumn = columns.includes("tenant")
|
||||
? "tenant"
|
||||
: columns.includes("tenant_id")
|
||||
? "tenant_id"
|
||||
: null
|
||||
|
||||
if (!tenantColumn) continue
|
||||
|
||||
const rows = await loadRows(client, table, `${quoteIdent(tenantColumn)} = $1`, [tenantId])
|
||||
addRows(tables, table, rows)
|
||||
}
|
||||
|
||||
const profileIds = collectIds(tables.auth_profiles || [], "id")
|
||||
const userIds = Array.from(new Set([
|
||||
...collectIds(tables.auth_tenant_users || [], "user_id"),
|
||||
...collectIds(tables.auth_profiles || [], "user_id"),
|
||||
...collectIds(tables.auth_user_roles || [], "user_id"),
|
||||
]))
|
||||
const roleIds = Array.from(new Set([
|
||||
...collectIds(tables.auth_roles || [], "id"),
|
||||
...collectIds(tables.auth_user_roles || [], "role_id"),
|
||||
]))
|
||||
|
||||
if (userIds.length) {
|
||||
addRows(tables, "auth_users", await loadRows(client, "auth_users", "id = any($1::uuid[])", [userIds]))
|
||||
}
|
||||
|
||||
if (roleIds.length) {
|
||||
addRows(tables, "auth_roles", await loadRows(client, "auth_roles", "id = any($1::uuid[])", [roleIds]))
|
||||
addRows(tables, "auth_role_permissions", await loadRows(client, "auth_role_permissions", "role_id = any($1::uuid[])", [roleIds]))
|
||||
}
|
||||
|
||||
if (profileIds.length) {
|
||||
addRows(tables, "auth_profile_branches", await loadRows(client, "auth_profile_branches", "profile_id = any($1::uuid[])", [profileIds]))
|
||||
addRows(tables, "auth_profile_teams", await loadRows(client, "auth_profile_teams", "profile_id = any($1::uuid[])", [profileIds]))
|
||||
}
|
||||
|
||||
const fileRows = tables.files || []
|
||||
const files = []
|
||||
|
||||
for (const file of fileRows) {
|
||||
if (!file.path) continue
|
||||
|
||||
files.push({
|
||||
id: file.id,
|
||||
path: file.path,
|
||||
name: file.name || null,
|
||||
mimeType: file.mimeType || null,
|
||||
size: file.size || null,
|
||||
contentBase64: await loadObjectAsBase64(file.path),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
format: "fedeo.tenant-full-export",
|
||||
version: 1,
|
||||
exportedAt: new Date().toISOString(),
|
||||
tenantId,
|
||||
tables,
|
||||
files,
|
||||
}
|
||||
} finally {
|
||||
client.release()
|
||||
}
|
||||
}
|
||||
|
||||
const restoreFiles = async (exportData: TenantFullExport) => {
|
||||
let restored = 0
|
||||
let skipped = 0
|
||||
|
||||
for (const file of exportData.files || []) {
|
||||
if (!file.path || !file.contentBase64) {
|
||||
skipped += 1
|
||||
continue
|
||||
}
|
||||
|
||||
const body = Buffer.from(file.contentBase64, "base64")
|
||||
await s3.send(new PutObjectCommand({
|
||||
Bucket: secrets.S3_BUCKET,
|
||||
Key: file.path,
|
||||
Body: body,
|
||||
ContentType: file.mimeType || "application/octet-stream",
|
||||
ContentLength: body.length,
|
||||
}))
|
||||
restored += 1
|
||||
}
|
||||
|
||||
return { restored, skipped }
|
||||
}
|
||||
|
||||
const insertRows = async (client: any, table: string, rows: Record<string, any>[], columns: string[]) => {
|
||||
if (!rows.length) return 0
|
||||
|
||||
let inserted = 0
|
||||
const availableColumns = new Set(columns)
|
||||
|
||||
for (const row of rows) {
|
||||
const rowColumns = Object.keys(row).filter((column) => availableColumns.has(column))
|
||||
if (!rowColumns.length) continue
|
||||
|
||||
const placeholders = rowColumns.map((_, index) => `$${index + 1}`).join(", ")
|
||||
const values = rowColumns.map((column) => row[column])
|
||||
|
||||
await client.query(
|
||||
`insert into ${quoteIdent(table)} (${rowColumns.map(quoteIdent).join(", ")}) values (${placeholders}) on conflict do nothing`,
|
||||
values
|
||||
)
|
||||
inserted += 1
|
||||
}
|
||||
|
||||
return inserted
|
||||
}
|
||||
|
||||
const refreshSequences = async (client: any, columnsByTable: Map<string, string[]>) => {
|
||||
for (const [table, columns] of columnsByTable.entries()) {
|
||||
if (!columns.includes("id")) continue
|
||||
|
||||
const sequenceResult = await client.query("select pg_get_serial_sequence($1, $2) as sequence_name", [`public.${table}`, "id"])
|
||||
const sequenceName = sequenceResult.rows[0]?.sequence_name
|
||||
if (!sequenceName) continue
|
||||
|
||||
await client.query(`
|
||||
select setval(
|
||||
$1::regclass,
|
||||
greatest(coalesce((select max(id) from ${quoteIdent(table)}), 1), 1),
|
||||
true
|
||||
)
|
||||
`, [sequenceName])
|
||||
}
|
||||
}
|
||||
|
||||
export const importTenantFullExport = async (server: FastifyInstance, exportData: TenantFullExport): Promise<ImportResult> => {
|
||||
if (exportData?.format !== "fedeo.tenant-full-export" || exportData.version !== 1) {
|
||||
throw new Error("Ungültiges FEDEO Mandantenexport-Format")
|
||||
}
|
||||
|
||||
const client = await pool.connect()
|
||||
const importOrder = [
|
||||
"tenants",
|
||||
"auth_users",
|
||||
"auth_roles",
|
||||
"auth_role_permissions",
|
||||
"auth_tenant_users",
|
||||
"auth_profiles",
|
||||
"auth_user_roles",
|
||||
"auth_profile_branches",
|
||||
"auth_profile_teams",
|
||||
]
|
||||
|
||||
try {
|
||||
const columnsByTable = await tableColumns(client)
|
||||
const tableNames = [
|
||||
...importOrder,
|
||||
...Object.keys(exportData.tables).filter((table) => !importOrder.includes(table)).sort(),
|
||||
].filter((table, index, all) => all.indexOf(table) === index)
|
||||
|
||||
await client.query("begin")
|
||||
await client.query("set local session_replication_role = replica")
|
||||
const files = await restoreFiles(exportData)
|
||||
|
||||
const importedTables: { table: string; rows: number }[] = []
|
||||
for (const table of tableNames) {
|
||||
const rows = exportData.tables[table] || []
|
||||
const columns = columnsByTable.get(table)
|
||||
if (!columns) continue
|
||||
|
||||
const count = await insertRows(client, table, rows, columns)
|
||||
importedTables.push({ table, rows: count })
|
||||
}
|
||||
|
||||
await refreshSequences(client, columnsByTable)
|
||||
await client.query("commit")
|
||||
|
||||
return {
|
||||
tenantId: exportData.tenantId,
|
||||
tables: importedTables,
|
||||
files,
|
||||
}
|
||||
} catch (err) {
|
||||
await client.query("rollback")
|
||||
throw err
|
||||
} finally {
|
||||
client.release()
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,13 @@ export type AdminOverview = {
|
||||
unassignedProfiles: AdminUserProfile[]
|
||||
}
|
||||
|
||||
export type TenantImportResult = {
|
||||
success: boolean
|
||||
tenantId: number
|
||||
tables: { table: string; rows: number }[]
|
||||
files: { restored: number; skipped: number }
|
||||
}
|
||||
|
||||
export const useAdmin = () => {
|
||||
const { $api } = useNuxtApp()
|
||||
|
||||
@@ -110,6 +117,19 @@ export const useAdmin = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const exportTenant = async (id: number): Promise<Blob> => {
|
||||
return await $api(`/api/admin/tenants/${id}/export`, {
|
||||
responseType: "blob",
|
||||
})
|
||||
}
|
||||
|
||||
const importTenant = async (body: Record<string, any>): Promise<TenantImportResult> => {
|
||||
return await $api("/api/admin/tenant-imports", {
|
||||
method: "POST",
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
getOverview,
|
||||
createUser,
|
||||
@@ -119,5 +139,7 @@ export const useAdmin = () => {
|
||||
createTenant,
|
||||
invitePortalUser,
|
||||
updateTenant,
|
||||
exportTenant,
|
||||
importTenant,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,19 @@ const admin = useAdmin()
|
||||
const tenantId = Number(route.params.id)
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const exportingTenant = ref(false)
|
||||
const importingTenant = ref(false)
|
||||
const creatingUser = ref(false)
|
||||
const createUserModalOpen = ref(false)
|
||||
const createdUserPassword = ref("")
|
||||
const importFileInput = ref<HTMLInputElement | null>(null)
|
||||
const lastImportResult = ref<null | {
|
||||
tenantId: number
|
||||
tableCount: number
|
||||
rowCount: number
|
||||
restoredFiles: number
|
||||
skippedFiles: number
|
||||
}>(null)
|
||||
|
||||
const tenantForm = ref<AdminTenant | null>(null)
|
||||
const assignedUsers = ref<AdminUser[]>([])
|
||||
@@ -79,6 +89,88 @@ const saveTenant = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const downloadTenantExport = async () => {
|
||||
if (!tenantForm.value || exportingTenant.value) return
|
||||
|
||||
exportingTenant.value = true
|
||||
|
||||
try {
|
||||
const blob = await admin.exportTenant(tenantForm.value.id)
|
||||
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`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
|
||||
toast.add({ title: "Mandantenexport erstellt", color: "green" })
|
||||
} catch (err: any) {
|
||||
console.error("[administration/tenants/export]", err)
|
||||
toast.add({
|
||||
title: "Mandant konnte nicht exportiert werden",
|
||||
description: err?.data?.error || err?.message || "Unbekannter Fehler",
|
||||
color: "red",
|
||||
})
|
||||
} finally {
|
||||
exportingTenant.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openImportFileDialog = () => {
|
||||
importFileInput.value?.click()
|
||||
}
|
||||
|
||||
const importTenantExport = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file || importingTenant.value) return
|
||||
|
||||
importingTenant.value = true
|
||||
lastImportResult.value = null
|
||||
|
||||
try {
|
||||
const rawContent = await file.text()
|
||||
const exportData = JSON.parse(rawContent)
|
||||
const result = await admin.importTenant(exportData)
|
||||
const rowCount = (result.tables || []).reduce((sum, table) => sum + table.rows, 0)
|
||||
|
||||
lastImportResult.value = {
|
||||
tenantId: result.tenantId,
|
||||
tableCount: result.tables?.length || 0,
|
||||
rowCount,
|
||||
restoredFiles: result.files?.restored || 0,
|
||||
skippedFiles: result.files?.skipped || 0,
|
||||
}
|
||||
|
||||
await fetchTenant()
|
||||
|
||||
toast.add({
|
||||
title: "Mandantenimport abgeschlossen",
|
||||
description: `${rowCount} Datensätze und ${lastImportResult.value.restoredFiles} Dateien verarbeitet.`,
|
||||
color: "green",
|
||||
})
|
||||
} catch (err: any) {
|
||||
console.error("[administration/tenants/import]", err)
|
||||
toast.add({
|
||||
title: "Mandant konnte nicht importiert werden",
|
||||
description: err?.data?.error || err?.message || "Unbekannter Fehler",
|
||||
color: "red",
|
||||
})
|
||||
} finally {
|
||||
importingTenant.value = false
|
||||
input.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const createTenantUser = async () => {
|
||||
if (!tenantForm.value || creatingUser.value) return
|
||||
|
||||
@@ -186,6 +278,59 @@ onMounted(async () => {
|
||||
</UForm>
|
||||
</UCard>
|
||||
|
||||
<UCard v-if="!loading && tenantForm" class="mt-3">
|
||||
<USeparator label="Backup und Umzug" />
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
<div class="border border-gray-200 dark:border-gray-800 rounded-lg p-4">
|
||||
<div class="font-medium">Full Export</div>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Erstellt eine portable JSON-Datei mit Mandantendaten, Benutzerzuordnungen, Rollen, Profilen und Dateiinhalten.
|
||||
</p>
|
||||
<UButton
|
||||
class="mt-4"
|
||||
icon="i-heroicons-arrow-down-tray"
|
||||
:loading="exportingTenant"
|
||||
@click="downloadTenantExport"
|
||||
>
|
||||
Export herunterladen
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="border border-gray-200 dark:border-gray-800 rounded-lg p-4">
|
||||
<div class="font-medium">Import</div>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Spielt einen FEDEO-Mandantenexport auf diesem Server ein. Bestehende Datensätze mit gleicher ID werden übersprungen.
|
||||
</p>
|
||||
<input
|
||||
ref="importFileInput"
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
class="hidden"
|
||||
@change="importTenantExport"
|
||||
>
|
||||
<UButton
|
||||
class="mt-4"
|
||||
icon="i-heroicons-arrow-up-tray"
|
||||
color="warning"
|
||||
:loading="importingTenant"
|
||||
@click="openImportFileDialog"
|
||||
>
|
||||
Export importieren
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
v-if="lastImportResult"
|
||||
class="mt-4"
|
||||
title="Letzter Import"
|
||||
:description="`Tenant ${lastImportResult.tenantId}: ${lastImportResult.rowCount} Datensätze aus ${lastImportResult.tableCount} Tabellen verarbeitet, ${lastImportResult.restoredFiles} Dateien wiederhergestellt, ${lastImportResult.skippedFiles} Dateien übersprungen.`"
|
||||
color="green"
|
||||
variant="soft"
|
||||
/>
|
||||
</UCard>
|
||||
|
||||
<UCard v-if="!loading && tenantForm" class="mt-3">
|
||||
<USeparator label="Zugeordnete Benutzer" />
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ const admin = useAdmin()
|
||||
|
||||
const loading = ref(true)
|
||||
const creatingTenant = ref(false)
|
||||
const importingTenant = ref(false)
|
||||
const createTenantModalOpen = ref(false)
|
||||
const importFileInput = ref<HTMLInputElement | null>(null)
|
||||
const tenants = ref<AdminTenant[]>([])
|
||||
const searchString = ref("")
|
||||
|
||||
@@ -89,6 +91,46 @@ const createTenant = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const openImportFileDialog = () => {
|
||||
importFileInput.value?.click()
|
||||
}
|
||||
|
||||
const importTenantExport = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
if (!file || importingTenant.value) return
|
||||
|
||||
importingTenant.value = true
|
||||
|
||||
try {
|
||||
const exportData = JSON.parse(await file.text())
|
||||
const result = await admin.importTenant(exportData)
|
||||
const rowCount = (result.tables || []).reduce((sum, table) => sum + table.rows, 0)
|
||||
|
||||
await fetchTenants()
|
||||
|
||||
toast.add({
|
||||
title: "Mandantenimport abgeschlossen",
|
||||
description: `Tenant ${result.tenantId}: ${rowCount} Datensätze und ${result.files?.restored || 0} Dateien verarbeitet.`,
|
||||
color: "green",
|
||||
})
|
||||
|
||||
if (result.tenantId) {
|
||||
await router.push(`/administration/tenants/${result.tenantId}`)
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("[administration/tenants/import]", err)
|
||||
toast.add({
|
||||
title: "Mandant konnte nicht importiert werden",
|
||||
description: err?.data?.error || err?.message || "Unbekannter Fehler",
|
||||
color: "red",
|
||||
})
|
||||
} finally {
|
||||
importingTenant.value = false
|
||||
input.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!auth.user?.is_admin) {
|
||||
await router.push("/")
|
||||
@@ -111,6 +153,22 @@ onMounted(async () => {
|
||||
<UButton icon="i-heroicons-plus" @click="createTenantModalOpen = true">
|
||||
Tenant
|
||||
</UButton>
|
||||
<input
|
||||
ref="importFileInput"
|
||||
type="file"
|
||||
accept="application/json,.json"
|
||||
class="hidden"
|
||||
@change="importTenantExport"
|
||||
>
|
||||
<UButton
|
||||
icon="i-heroicons-arrow-up-tray"
|
||||
color="warning"
|
||||
variant="soft"
|
||||
:loading="importingTenant"
|
||||
@click="openImportFileDialog"
|
||||
>
|
||||
Import
|
||||
</UButton>
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user