331 lines
11 KiB
Vue
331 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import type { AdminRole, AdminUser, AdminUserProfile } from "~/composables/useAdmin"
|
|
|
|
const auth = useAuthStore()
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const toast = useToast()
|
|
const admin = useAdmin()
|
|
|
|
const userId = route.params.id as string
|
|
const loading = ref(true)
|
|
const saving = ref(false)
|
|
|
|
const userForm = ref<AdminUser | null>(null)
|
|
const roles = ref<AdminRole[]>([])
|
|
const tenants = ref<{ id: number; name: string; short: string }[]>([])
|
|
const unassignedProfiles = ref<AdminUserProfile[]>([])
|
|
|
|
const tenantOptions = computed(() =>
|
|
tenants.value.map((tenant) => ({
|
|
label: `${tenant.name} (${tenant.short})`,
|
|
value: tenant.id,
|
|
}))
|
|
)
|
|
|
|
const getRoleOptionsForTenant = (tenantId: number) =>
|
|
roles.value
|
|
.filter((role) => role.tenant_id === null || role.tenant_id === tenantId)
|
|
.map((role) => ({
|
|
label: role.tenant_id === null ? `${role.name} (global)` : role.name,
|
|
value: role.id,
|
|
}))
|
|
|
|
const getFreeProfilesForTenant = (tenantId: number) =>
|
|
unassignedProfiles.value.filter((profile) => profile.tenant_id === tenantId)
|
|
|
|
const normalizeUserAssignments = () => {
|
|
if (!userForm.value) return
|
|
|
|
const uniqueTenantIds = Array.from(new Set((userForm.value.tenant_ids || []).map(Number))).sort((a, b) => a - b)
|
|
const assignmentsByTenant = new Map<number, string>()
|
|
const profileAssignmentByTenant = new Map<number, string | null>()
|
|
|
|
for (const assignment of userForm.value.role_assignments || []) {
|
|
if (!uniqueTenantIds.includes(Number(assignment.tenant_id))) continue
|
|
if (assignmentsByTenant.has(Number(assignment.tenant_id))) continue
|
|
assignmentsByTenant.set(Number(assignment.tenant_id), assignment.role_id)
|
|
}
|
|
|
|
for (const assignment of userForm.value.profile_assignments || []) {
|
|
if (!uniqueTenantIds.includes(Number(assignment.tenant_id))) continue
|
|
profileAssignmentByTenant.set(Number(assignment.tenant_id), assignment.profile_id || null)
|
|
}
|
|
|
|
userForm.value.tenant_ids = uniqueTenantIds
|
|
userForm.value.role_assignments = uniqueTenantIds
|
|
.map((tenantId) => {
|
|
const roleId = assignmentsByTenant.get(tenantId)
|
|
return roleId ? { tenant_id: tenantId, role_id: roleId } : null
|
|
})
|
|
.filter(Boolean) as { tenant_id: number; role_id: string }[]
|
|
userForm.value.profile_assignments = uniqueTenantIds.map((tenantId) => ({
|
|
tenant_id: tenantId,
|
|
profile_id: profileAssignmentByTenant.get(tenantId) || null,
|
|
}))
|
|
}
|
|
|
|
const updateUserTenants = (tenantIds: number[] = []) => {
|
|
if (!userForm.value) return
|
|
userForm.value.tenant_ids = tenantIds
|
|
normalizeUserAssignments()
|
|
}
|
|
|
|
const setRoleForTenant = (tenantId: number, roleId?: string | null) => {
|
|
if (!userForm.value) return
|
|
userForm.value.role_assignments = (userForm.value.role_assignments || []).filter((assignment) => assignment.tenant_id !== tenantId)
|
|
if (roleId) {
|
|
userForm.value.role_assignments.push({ tenant_id: tenantId, role_id: roleId })
|
|
}
|
|
}
|
|
|
|
const getRoleForTenant = (tenantId: number) =>
|
|
userForm.value?.role_assignments?.find((assignment) => assignment.tenant_id === tenantId)?.role_id || null
|
|
|
|
const setProfileAssignmentForTenant = (tenantId: number, profileId?: string | null) => {
|
|
if (!userForm.value) return
|
|
userForm.value.profile_assignments = (userForm.value.profile_assignments || []).filter((assignment) => assignment.tenant_id !== tenantId)
|
|
userForm.value.profile_assignments.push({
|
|
tenant_id: tenantId,
|
|
profile_id: profileId || null,
|
|
})
|
|
}
|
|
|
|
const getProfileAssignmentForTenant = (tenantId: number) =>
|
|
userForm.value?.profile_assignments?.find((assignment) => assignment.tenant_id === tenantId)?.profile_id || null
|
|
|
|
const fetchUser = async () => {
|
|
loading.value = true
|
|
|
|
try {
|
|
const overview = await admin.getOverview()
|
|
roles.value = overview.roles
|
|
tenants.value = overview.tenants
|
|
unassignedProfiles.value = overview.unassignedProfiles
|
|
|
|
const user = overview.users.find((entry) => entry.id === userId)
|
|
if (!user) {
|
|
toast.add({ title: "Benutzer nicht gefunden", color: "red" })
|
|
await router.push("/administration/users")
|
|
return
|
|
}
|
|
|
|
userForm.value = {
|
|
...user,
|
|
profile_defaults: { ...user.profile_defaults },
|
|
tenant_ids: [...user.tenant_ids],
|
|
role_assignments: [...user.role_assignments],
|
|
profile_assignments: [...(user.profile_assignments || [])],
|
|
profiles: [...user.profiles],
|
|
}
|
|
normalizeUserAssignments()
|
|
} catch (err: any) {
|
|
console.error("[administration/users/show]", err)
|
|
toast.add({
|
|
title: "Benutzer konnte nicht geladen werden",
|
|
description: err?.data?.error || err?.message || "Unbekannter Fehler",
|
|
color: "red",
|
|
})
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const saveUser = async () => {
|
|
if (!userForm.value || saving.value) return
|
|
|
|
saving.value = true
|
|
normalizeUserAssignments()
|
|
|
|
try {
|
|
await admin.updateUser(userForm.value.id, {
|
|
email: userForm.value.email,
|
|
multiTenant: userForm.value.multiTenant,
|
|
must_change_password: userForm.value.must_change_password,
|
|
is_admin: userForm.value.is_admin,
|
|
})
|
|
|
|
await admin.updateUserAccess(userForm.value.id, {
|
|
tenant_ids: userForm.value.tenant_ids,
|
|
role_assignments: userForm.value.role_assignments,
|
|
profile_defaults: userForm.value.profile_defaults,
|
|
profile_assignments: userForm.value.profile_assignments,
|
|
})
|
|
|
|
await fetchUser()
|
|
await auth.fetchMe()
|
|
|
|
toast.add({ title: "Benutzer gespeichert", color: "green" })
|
|
} catch (err: any) {
|
|
console.error("[administration/users/save]", err)
|
|
toast.add({
|
|
title: "Benutzer konnte nicht gespeichert werden",
|
|
description: err?.data?.error || err?.message || "Unbekannter Fehler",
|
|
color: "red",
|
|
})
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (!auth.user?.is_admin) {
|
|
await router.push("/")
|
|
return
|
|
}
|
|
|
|
await fetchUser()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<UDashboardNavbar title="Administration: Benutzer">
|
|
<template #left>
|
|
<UButton icon="i-heroicons-chevron-left" variant="outline" @click="router.push('/administration/users')">
|
|
Benutzer
|
|
</UButton>
|
|
</template>
|
|
<template #right>
|
|
<UButton color="primary" :loading="saving" @click="saveUser">
|
|
Speichern
|
|
</UButton>
|
|
</template>
|
|
</UDashboardNavbar>
|
|
|
|
<UDashboardPanelContent>
|
|
<UCard v-if="!loading && userForm">
|
|
<div class="flex items-start justify-between gap-4 mb-6">
|
|
<div>
|
|
<h2 class="text-xl font-semibold">{{ userForm.display_name }}</h2>
|
|
<p class="text-sm text-gray-500">{{ userForm.email }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<USeparator label="Benutzer" />
|
|
|
|
<UForm :state="userForm" class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
|
|
<UFormField label="E-Mail">
|
|
<UInput v-model="userForm.email" />
|
|
</UFormField>
|
|
|
|
<UFormField label="Profil Vorname">
|
|
<UInput v-model="userForm.profile_defaults.first_name" />
|
|
</UFormField>
|
|
|
|
<UFormField label="Profil Nachname">
|
|
<UInput v-model="userForm.profile_defaults.last_name" />
|
|
</UFormField>
|
|
|
|
<UFormField label="Tenants">
|
|
<USelectMenu
|
|
:model-value="userForm.tenant_ids"
|
|
:items="tenantOptions"
|
|
value-key="value"
|
|
label-key="label"
|
|
multiple
|
|
@update:model-value="updateUserTenants"
|
|
/>
|
|
</UFormField>
|
|
|
|
<UFormField label="Administrative Freigabe">
|
|
<div class="flex items-center gap-3 h-10">
|
|
<USwitch v-model="userForm.is_admin" />
|
|
<span class="text-sm text-gray-600">Darf Administrationsseiten und Admin-API nutzen</span>
|
|
</div>
|
|
</UFormField>
|
|
|
|
<UFormField label="Multi-Tenant">
|
|
<div class="flex items-center gap-3 h-10">
|
|
<USwitch v-model="userForm.multiTenant" />
|
|
<span class="text-sm text-gray-600">Mehrere Tenant-Zuordnungen erlauben</span>
|
|
</div>
|
|
</UFormField>
|
|
|
|
<UFormField label="Passwortwechsel erzwingen">
|
|
<div class="flex items-center gap-3 h-10">
|
|
<USwitch v-model="userForm.must_change_password" />
|
|
<span class="text-sm text-gray-600">Beim nächsten Login muss das Passwort geändert werden</span>
|
|
</div>
|
|
</UFormField>
|
|
</UForm>
|
|
</UCard>
|
|
|
|
<UCard v-if="!loading && userForm" class="mt-3">
|
|
<USeparator label="Rollen und Profile" />
|
|
|
|
<div v-if="userForm.tenant_ids.length" class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
|
<UCard
|
|
v-for="tenantId in userForm.tenant_ids"
|
|
:key="tenantId"
|
|
class="border border-gray-200"
|
|
>
|
|
<div class="space-y-3">
|
|
<div>
|
|
<div class="font-medium">
|
|
{{ tenants.find((tenant) => tenant.id === tenantId)?.name || `Tenant ${tenantId}` }}
|
|
</div>
|
|
<div class="text-sm text-gray-500">
|
|
{{ tenants.find((tenant) => tenant.id === tenantId)?.short || "" }}
|
|
</div>
|
|
</div>
|
|
|
|
<UFormField label="Rolle">
|
|
<USelectMenu
|
|
:model-value="getRoleForTenant(tenantId)"
|
|
:items="getRoleOptionsForTenant(tenantId)"
|
|
value-key="value"
|
|
label-key="label"
|
|
placeholder="Rolle auswählen"
|
|
@update:model-value="(value) => setRoleForTenant(tenantId, value)"
|
|
/>
|
|
</UFormField>
|
|
|
|
<UFormField label="Freies Profil">
|
|
<USelectMenu
|
|
:model-value="getProfileAssignmentForTenant(tenantId)"
|
|
:items="[
|
|
{ label: 'Neues Profil erzeugen', value: null },
|
|
...getFreeProfilesForTenant(tenantId).map((profile) => ({
|
|
label: profile.full_name || `${profile.first_name} ${profile.last_name}`,
|
|
value: profile.id,
|
|
}))
|
|
]"
|
|
value-key="value"
|
|
label-key="label"
|
|
placeholder="Profil auswählen"
|
|
@update:model-value="(value) => setProfileAssignmentForTenant(tenantId, value)"
|
|
/>
|
|
</UFormField>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
|
|
<UAlert
|
|
v-else-if="!loading"
|
|
title="Keine Tenant-Zuordnung"
|
|
description="Weise dem Benutzer zuerst mindestens einen Tenant zu."
|
|
color="amber"
|
|
variant="soft"
|
|
class="mt-4"
|
|
/>
|
|
</UCard>
|
|
|
|
<UCard v-if="!loading && userForm" class="mt-3">
|
|
<USeparator label="Profile im System" />
|
|
|
|
<div class="flex flex-wrap gap-2 mt-4">
|
|
<UBadge
|
|
v-for="profile in userForm.profiles"
|
|
:key="profile.id"
|
|
variant="subtle"
|
|
color="gray"
|
|
>
|
|
{{ profile.full_name || `${profile.first_name} ${profile.last_name}` }} · Tenant {{ profile.tenant_id }}
|
|
</UBadge>
|
|
</div>
|
|
</UCard>
|
|
|
|
<USkeleton v-if="loading" class="h-80" />
|
|
</UDashboardPanelContent>
|
|
</template>
|