337 lines
9.9 KiB
TypeScript
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,
|
|
}
|
|
}
|