Files
vtuber-awards/frontend/src/components/admin/useAdminTeamManager.ts
T
2026-07-09 23:47:07 +02:00

337 lines
9.9 KiB
TypeScript

import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { ApiRequestError, api } from '../../lib/api'
import type { AdminTeamMember, AdminTeamPermission, AdminTeamRole } from '../../types/awards'
export interface TeamMemberForm {
login: string
displayName: string
role: string
isActive: boolean
}
const emptyMemberForm = (): TeamMemberForm => ({
login: '',
displayName: '',
role: 'member',
isActive: true,
})
const TEAM_PRESENCE_REFRESH_MS = 30_000
export function useAdminTeamManager() {
const loading = ref(true)
const saving = ref(false)
const refreshingPresence = ref(false)
const lastPresenceRefreshAt = ref<Date | null>(null)
const errorMessage = ref('')
const successMessage = ref('')
const generatedPassword = ref('')
const generatedPasswordLogin = ref('')
const confirmDeleteMemberId = ref<number | null>(null)
const members = ref<AdminTeamMember[]>([])
const roles = ref<AdminTeamRole[]>([])
const permissions = ref<AdminTeamPermission[]>([])
const editingMemberId = ref<number | null>(null)
const rolePermissionDrafts = ref<Record<string, string[]>>({})
const savedRoleSnapshot = ref('')
const memberForm = reactive<TeamMemberForm>(emptyMemberForm())
const activeMembers = computed(() => members.value.filter((member) => member.isActive).length)
const pendingPasswordChanges = computed(() => members.value.filter((member) => member.mustChangePassword).length)
const roleOptions = computed(() => roles.value.map((role) => ({ value: role.key, label: role.label })))
const permissionGroups = computed(() => {
const grouped = new Map<string, AdminTeamPermission[]>()
for (const permission of permissions.value) {
const key = permission.groupLabel || 'Sonstiges'
const current = grouped.get(key)
if (current) current.push(permission)
else grouped.set(key, [permission])
}
return [...grouped.entries()].map(([label, items]) => ({
label,
items,
}))
})
const selectedMember = computed(() => members.value.find((member) => member.id === editingMemberId.value) ?? null)
const assignableRoleOptions = computed(() => roleOptions.value.filter((role) =>
role.value !== 'creator' || selectedMember.value?.role === 'creator',
))
const hasRoleChanges = computed(() =>
Boolean(savedRoleSnapshot.value) && JSON.stringify(rolePermissionDrafts.value) !== savedRoleSnapshot.value,
)
function resetMessages() {
errorMessage.value = ''
successMessage.value = ''
}
function applyTeamResponse(response: { members: AdminTeamMember[]; roles: AdminTeamRole[]; permissions: AdminTeamPermission[] }) {
members.value = response.members
roles.value = response.roles
permissions.value = response.permissions
rolePermissionDrafts.value = Object.fromEntries(
response.roles.map((role) => [role.key, [...role.permissionKeys]]),
)
savedRoleSnapshot.value = JSON.stringify(rolePermissionDrafts.value)
}
async function loadTeam(options: { silent?: boolean } = {}) {
const silent = options.silent === true
const showLoading = !silent || members.value.length === 0
if (showLoading) {
loading.value = true
} else {
refreshingPresence.value = true
}
if (!silent) {
resetMessages()
}
try {
applyTeamResponse(await api.getAdminTeam())
lastPresenceRefreshAt.value = new Date()
} catch (error) {
if (!silent) {
errorMessage.value = error instanceof ApiRequestError
? error.message
: 'Team-Daten konnten nicht geladen werden.'
}
} finally {
if (showLoading) {
loading.value = false
}
refreshingPresence.value = false
}
}
function startCreateMember() {
editingMemberId.value = null
Object.assign(memberForm, emptyMemberForm())
generatedPassword.value = ''
generatedPasswordLogin.value = ''
resetMessages()
}
function startEditMember(member: AdminTeamMember) {
editingMemberId.value = member.id
memberForm.login = member.login
memberForm.displayName = member.displayName
memberForm.role = member.role
memberForm.isActive = member.isActive
generatedPassword.value = ''
generatedPasswordLogin.value = ''
resetMessages()
}
function validateMemberForm() {
if (!memberForm.login.trim() && editingMemberId.value === null) {
errorMessage.value = 'Bitte gib einen Login an.'
return false
}
if (!memberForm.displayName.trim()) {
errorMessage.value = 'Bitte gib einen Anzeigenamen an.'
return false
}
return true
}
async function saveMember() {
resetMessages()
generatedPassword.value = ''
generatedPasswordLogin.value = ''
if (!validateMemberForm()) return false
saving.value = true
try {
if (editingMemberId.value === null) {
const result = await api.createAdminTeamMember({
login: memberForm.login,
displayName: memberForm.displayName,
role: memberForm.role,
})
await loadTeam({ silent: true })
generatedPassword.value = result.generatedPassword
generatedPasswordLogin.value = memberForm.login
successMessage.value = 'Team-Login wurde erstellt. Das temporäre Passwort ist nur jetzt sichtbar.'
} else {
await api.updateAdminTeamMember(editingMemberId.value, {
login: memberForm.login,
displayName: memberForm.displayName,
role: memberForm.role,
isActive: memberForm.isActive,
})
await loadTeam({ silent: true })
successMessage.value = 'Team-Mitglied wurde gespeichert.'
}
return true
} catch (error) {
errorMessage.value = error instanceof ApiRequestError
? error.message
: 'Team-Mitglied konnte nicht gespeichert werden.'
return false
} finally {
saving.value = false
}
}
async function resetMemberPassword(member: AdminTeamMember) {
resetMessages()
generatedPassword.value = ''
generatedPasswordLogin.value = ''
saving.value = true
try {
const result = await api.resetAdminTeamMemberPassword(member.id)
await loadTeam({ silent: true })
generatedPassword.value = result.generatedPassword
generatedPasswordLogin.value = member.login
successMessage.value = 'Passwort wurde zurückgesetzt. Das Mitglied muss es beim nächsten Login ändern.'
} catch (error) {
errorMessage.value = error instanceof ApiRequestError
? error.message
: 'Passwort konnte nicht zurückgesetzt werden.'
} finally {
saving.value = false
}
}
function requestDeleteMember(member: AdminTeamMember) {
confirmDeleteMemberId.value = member.id
resetMessages()
}
function cancelDeleteMember() {
confirmDeleteMemberId.value = null
}
async function deleteMember(member: AdminTeamMember) {
resetMessages()
generatedPassword.value = ''
generatedPasswordLogin.value = ''
saving.value = true
try {
await api.deleteAdminTeamMember(member.id)
confirmDeleteMemberId.value = null
if (editingMemberId.value === member.id) {
startCreateMember()
}
await loadTeam({ silent: true })
successMessage.value = 'Team-Mitglied wurde gelöscht.'
} catch (error) {
errorMessage.value = error instanceof ApiRequestError
? error.message
: 'Team-Mitglied konnte nicht gelöscht werden.'
} finally {
saving.value = false
}
}
function roleHasPermission(roleKey: string, permissionKey: string) {
return rolePermissionDrafts.value[roleKey]?.includes(permissionKey) ?? false
}
function setRolePermission(roleKey: string, permissionKey: string, checked: boolean) {
if (roleKey === 'owner') return
const current = new Set(rolePermissionDrafts.value[roleKey] ?? [])
if (checked) {
current.add(permissionKey)
} else {
current.delete(permissionKey)
}
rolePermissionDrafts.value = {
...rolePermissionDrafts.value,
[roleKey]: [...current].sort(),
}
}
async function saveRolePermissions() {
resetMessages()
saving.value = true
try {
const result = await api.updateAdminTeamRoles({
roles: roles.value.map((role) => ({
key: role.key,
permissionKeys: rolePermissionDrafts.value[role.key] ?? [],
})),
})
roles.value = result.roles
rolePermissionDrafts.value = Object.fromEntries(
result.roles.map((role) => [role.key, [...role.permissionKeys]]),
)
savedRoleSnapshot.value = JSON.stringify(rolePermissionDrafts.value)
successMessage.value = 'Rollen und Berechtigungen wurden gespeichert.'
} catch (error) {
errorMessage.value = error instanceof ApiRequestError
? error.message
: 'Berechtigungen konnten nicht gespeichert werden.'
} finally {
saving.value = false
}
}
let presenceRefreshTimer: ReturnType<typeof window.setInterval> | null = null
onMounted(() => {
void loadTeam()
presenceRefreshTimer = window.setInterval(() => {
if (!saving.value) {
void loadTeam({ silent: true })
}
}, TEAM_PRESENCE_REFRESH_MS)
})
onBeforeUnmount(() => {
if (presenceRefreshTimer) {
window.clearInterval(presenceRefreshTimer)
presenceRefreshTimer = null
}
})
return {
loading,
saving,
refreshingPresence,
lastPresenceRefreshAt,
errorMessage,
successMessage,
generatedPassword,
generatedPasswordLogin,
confirmDeleteMemberId,
members,
roles,
permissions,
permissionGroups,
memberForm,
editingMemberId,
selectedMember,
activeMembers,
pendingPasswordChanges,
roleOptions,
assignableRoleOptions,
hasRoleChanges,
loadTeam,
startCreateMember,
startEditMember,
saveMember,
resetMemberPassword,
requestDeleteMember,
cancelDeleteMember,
deleteMember,
roleHasPermission,
setRolePermission,
saveRolePermissions,
}
}