Files
FEDEO/frontend/pages/administration/tenants/index.vue
florianfederspiel bb3b842be1
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 18s
Build and Push Docker Images / build-frontend (push) Successful in 51s
Build and Push Docker Images / build-docs (push) Successful in 11s
KI-AGENT: Ergänze Mandantenexport und Import
2026-05-18 21:23:18 +02:00

219 lines
5.8 KiB
Vue

<script setup lang="ts">
import type { AdminTenant } from "~/composables/useAdmin"
const auth = useAuthStore()
const toast = useToast()
const router = useRouter()
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("")
const createTenantForm = ref({
name: "",
short: "",
})
const templateColumns = [
{ key: "name", label: "Tenant" },
{ key: "short", label: "Kürzel" },
{ key: "user_count", label: "Benutzer" },
]
const filteredRows = computed(() => {
const search = searchString.value.trim().toLowerCase()
if (!search) return tenants.value
return tenants.value.filter((tenant) =>
[tenant.name, tenant.short]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(search))
)
})
const fetchTenants = async () => {
loading.value = true
try {
const overview = await admin.getOverview()
tenants.value = overview.tenants
} catch (err: any) {
console.error("[administration/tenants/index]", err)
toast.add({
title: "Tenants konnten nicht geladen werden",
description: err?.data?.error || err?.message || "Unbekannter Fehler",
color: "red",
})
} finally {
loading.value = false
}
}
const createTenant = async () => {
if (creatingTenant.value) return
creatingTenant.value = true
try {
const response = await admin.createTenant(createTenantForm.value)
createTenantModalOpen.value = false
createTenantForm.value = {
name: "",
short: "",
}
await fetchTenants()
toast.add({
title: "Tenant angelegt",
description: "Standardordner und Datei-Tags wurden erstellt.",
color: "green",
})
if (response.tenant?.id) {
await router.push(`/administration/tenants/${response.tenant.id}`)
}
} catch (err: any) {
console.error("[administration/tenants/create]", err)
toast.add({
title: "Tenant konnte nicht angelegt werden",
description: err?.data?.error || err?.message || "Unbekannter Fehler",
color: "red",
})
} finally {
creatingTenant.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
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("/")
return
}
await fetchTenants()
})
</script>
<template>
<UDashboardNavbar title="Administration: Tenants" :badge="filteredRows.length">
<template #right>
<UInput
v-model="searchString"
icon="i-heroicons-magnifying-glass"
placeholder="Tenants suchen"
class="hidden lg:block"
/>
<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>
<UTable
:data="filteredRows"
:columns="normalizeTableColumns(templateColumns)"
:loading="loading"
:on-select="(row) => router.push(`/administration/tenants/${row.original?.id || row.id}`)"
:empty="{ icon: 'i-heroicons-building-office-2', label: 'Keine Tenants gefunden' }"
/>
<UModal v-model:open="createTenantModalOpen">
<template #content>
<UCard>
<template #header>
<div class="text-lg font-semibold">Tenant anlegen</div>
</template>
<UForm :state="createTenantForm" class="space-y-4" @submit.prevent="createTenant">
<UFormField label="Name">
<UInput v-model="createTenantForm.name" />
</UFormField>
<UFormField label="Kürzel">
<UInput v-model="createTenantForm.short" />
</UFormField>
<UAlert
title="Seed-Daten"
description="Beim Anlegen werden Standard-Datei-Tags sowie Systemordner mit Jahresunterordnern erstellt."
color="primary"
variant="soft"
/>
<div class="flex justify-end gap-3 pt-2">
<UButton color="gray" variant="soft" @click="createTenantModalOpen = false">
Abbrechen
</UButton>
<UButton type="submit" color="primary" :loading="creatingTenant">
Tenant anlegen
</UButton>
</div>
</UForm>
</UCard>
</template>
</UModal>
</template>