Ersetzt ungültige UTable-Empty-Props durch einen gemeinsamen Empty-State-Slot, damit leere Tabellen keine Objekt-/JSON-Ausgabe mehr anzeigen.
208 lines
5.6 KiB
Vue
208 lines
5.6 KiB
Vue
<script setup lang="ts">
|
|
import type { AdminUser } from "~/composables/useAdmin"
|
|
|
|
const auth = useAuthStore()
|
|
const toast = useToast()
|
|
const router = useRouter()
|
|
const admin = useAdmin()
|
|
|
|
const loading = ref(true)
|
|
const creatingUser = ref(false)
|
|
const createUserModalOpen = ref(false)
|
|
const createdUserPassword = ref("")
|
|
const users = ref<AdminUser[]>([])
|
|
const searchString = ref("")
|
|
|
|
const createUserForm = ref({
|
|
email: "",
|
|
password: "",
|
|
first_name: "",
|
|
last_name: "",
|
|
is_admin: false,
|
|
multiTenant: true,
|
|
})
|
|
|
|
const templateColumns = [
|
|
{ key: "display_name", label: "Benutzer" },
|
|
{ key: "email", label: "E-Mail" },
|
|
{ key: "tenant_count", label: "Tenants" },
|
|
{ key: "is_admin", label: "Admin" },
|
|
]
|
|
|
|
const filteredRows = computed(() => {
|
|
const search = searchString.value.trim().toLowerCase()
|
|
const rows = users.value.map((user) => ({
|
|
...user,
|
|
tenant_count: user.tenant_ids.length,
|
|
is_admin: user.is_admin ? "Ja" : "Nein",
|
|
}))
|
|
|
|
if (!search) return rows
|
|
|
|
return rows.filter((row) =>
|
|
[row.display_name, row.email]
|
|
.filter(Boolean)
|
|
.some((value) => String(value).toLowerCase().includes(search))
|
|
)
|
|
})
|
|
|
|
const fetchUsers = async () => {
|
|
loading.value = true
|
|
|
|
try {
|
|
const overview = await admin.getOverview()
|
|
users.value = overview.users
|
|
} catch (err: any) {
|
|
console.error("[administration/users/index]", err)
|
|
toast.add({
|
|
title: "Benutzer konnten nicht geladen werden",
|
|
description: err?.data?.error || err?.message || "Unbekannter Fehler",
|
|
color: "red",
|
|
})
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const createUser = async () => {
|
|
if (creatingUser.value) return
|
|
|
|
creatingUser.value = true
|
|
|
|
try {
|
|
const response = await admin.createUser(createUserForm.value)
|
|
|
|
createdUserPassword.value = response.initialPassword || ""
|
|
createUserModalOpen.value = false
|
|
createUserForm.value = {
|
|
email: "",
|
|
password: "",
|
|
first_name: "",
|
|
last_name: "",
|
|
is_admin: false,
|
|
multiTenant: true,
|
|
}
|
|
|
|
await fetchUsers()
|
|
|
|
toast.add({
|
|
title: "Benutzer angelegt",
|
|
description: createdUserPassword.value ? `Initialpasswort: ${createdUserPassword.value}` : undefined,
|
|
color: "green",
|
|
})
|
|
|
|
if (response.user?.id) {
|
|
await router.push(`/administration/users/${response.user.id}`)
|
|
}
|
|
} catch (err: any) {
|
|
console.error("[administration/users/create]", err)
|
|
toast.add({
|
|
title: "Benutzer konnte nicht angelegt werden",
|
|
description: err?.data?.error || err?.message || "Unbekannter Fehler",
|
|
color: "red",
|
|
})
|
|
} finally {
|
|
creatingUser.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (!auth.user?.is_admin) {
|
|
await router.push("/")
|
|
return
|
|
}
|
|
|
|
await fetchUsers()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<UDashboardNavbar title="Administration: Benutzer" :badge="filteredRows.length">
|
|
<template #right>
|
|
<UInput
|
|
v-model="searchString"
|
|
icon="i-heroicons-magnifying-glass"
|
|
placeholder="Benutzer suchen"
|
|
class="hidden lg:block"
|
|
/>
|
|
<UButton icon="i-heroicons-plus" @click="createUserModalOpen = true">
|
|
Benutzer
|
|
</UButton>
|
|
</template>
|
|
</UDashboardNavbar>
|
|
|
|
<UTable
|
|
:data="filteredRows"
|
|
:columns="normalizeTableColumns(templateColumns)"
|
|
:loading="loading"
|
|
:on-select="(row) => router.push(`/administration/users/${row.original?.id || row.id}`)"
|
|
>
|
|
<template #empty>
|
|
<TableEmptyState label="Keine Benutzer gefunden" icon="i-heroicons-users" />
|
|
</template>
|
|
</UTable>
|
|
|
|
<UModal v-model:open="createUserModalOpen">
|
|
<template #content>
|
|
<UCard>
|
|
<template #header>
|
|
<div class="text-lg font-semibold">Benutzer anlegen</div>
|
|
</template>
|
|
|
|
<UForm :state="createUserForm" class="space-y-4" @submit.prevent="createUser">
|
|
<UFormField label="E-Mail">
|
|
<UInput v-model="createUserForm.email" type="email" />
|
|
</UFormField>
|
|
|
|
<UFormField label="Initialpasswort">
|
|
<UInput v-model="createUserForm.password" placeholder="Leer lassen für automatisches Passwort" />
|
|
</UFormField>
|
|
|
|
<UFormField label="Vorname für neues Profil">
|
|
<UInput v-model="createUserForm.first_name" />
|
|
</UFormField>
|
|
|
|
<UFormField label="Nachname für neues Profil">
|
|
<UInput v-model="createUserForm.last_name" />
|
|
</UFormField>
|
|
|
|
<UFormField label="Administrative Freigabe">
|
|
<div class="flex items-center gap-3 h-10">
|
|
<USwitch v-model="createUserForm.is_admin" />
|
|
<span class="text-sm text-gray-600">Benutzer darf die Administration öffnen</span>
|
|
</div>
|
|
</UFormField>
|
|
|
|
<UFormField label="Multi-Tenant">
|
|
<div class="flex items-center gap-3 h-10">
|
|
<USwitch v-model="createUserForm.multiTenant" />
|
|
<span class="text-sm text-gray-600">Mehrere Tenant-Zuordnungen erlauben</span>
|
|
</div>
|
|
</UFormField>
|
|
|
|
<div class="flex justify-end gap-3 pt-2">
|
|
<UButton color="gray" variant="soft" @click="createUserModalOpen = false">
|
|
Abbrechen
|
|
</UButton>
|
|
<UButton type="submit" color="primary" :loading="creatingUser">
|
|
Benutzer anlegen
|
|
</UButton>
|
|
</div>
|
|
</UForm>
|
|
</UCard>
|
|
</template>
|
|
</UModal>
|
|
|
|
<div class="mx-5 mb-5">
|
|
<UAlert
|
|
v-if="createdUserPassword"
|
|
title="Initialpasswort für neuen Benutzer"
|
|
:description="createdUserPassword"
|
|
color="amber"
|
|
variant="soft"
|
|
close-button
|
|
@close="createdUserPassword = ''"
|
|
/>
|
|
</div>
|
|
</template>
|