import { asError } from '@/utils/errorTypes'; import React, { useState, useMemo } from 'react'; // TODO: P2-F22 — Replace hardcoded LEGACY_ROLES with /roles API import { useTranslation } from 'react-i18next'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { useUsers, useCreateUser, useUpdateUser, useDeleteUser, useRoles } from '@/api/hooks'; import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { Input } from '@/components/ui/Input'; import { Select } from '@/components/ui/Select'; import { EmptyState } from '@/components/ui/EmptyState'; import { Skeleton } from '@/components/ui/Skeleton'; import { Avatar } from '@/components/ui/Avatar'; import { Modal } from '@/components/ui/Modal'; import { ConfirmDialog } from '@/components/ui/ConfirmDialog'; import { useToast } from '@/components/ui/Toast'; const LEGACY_ROLES = [ { value: 'admin', label: 'Administrator' }, { value: 'manager', label: 'Manager' }, { value: 'user', label: 'Mitarbeiter' }, { value: 'guest', label: 'Gast' }, { value: 'viewer', label: 'Viewer' }, ]; // ── Zod Schema ── const inviteSchema = z.object({ name: z.string().min(1, 'required'), email: z.string().min(1, 'required').email('invalidEmail'), password: z.string().min(8, 'passwordTooShort'), role_id: z.string().optional().default(''), }); type InviteFormData = z.infer; export function SettingsUsersPage() { const { t } = useTranslation(); const toast = useToast(); const { data, isLoading } = useUsers(1, 100); const { data: rolesData } = useRoles(); const createUserMutation = useCreateUser(); const updateUserMutation = useUpdateUser(); const deleteUserMutation = useDeleteUser(); const [inviteOpen, setInviteOpen] = useState(false); const [confirmDeactivate, setConfirmDeactivate] = useState(null); const [confirmDelete, setConfirmDelete] = useState(null); const { register, handleSubmit, reset, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(inviteSchema), defaultValues: { name: '', email: '', password: '', role_id: '' }, }); const users = data?.items ?? []; const customRoles = rolesData?.items ?? []; const roleOptions = useMemo(() => { const options = [...LEGACY_ROLES]; for (const role of customRoles) { options.push({ value: `role_id:${role.id}`, label: role.name }); } return options; }, [customRoles]); const roleLabelMap = useMemo(() => { const map = new Map(); for (const opt of roleOptions) { map.set(opt.value, opt.label); } return map; }, [roleOptions]); const getRoleValueForUser = (user: any): string => { if (user.role_id) { const key = `role_id:${user.role_id}`; if (roleLabelMap.has(key)) return key; } if (user.role) { const legacy = LEGACY_ROLES.find((r) => r.value === user.role); if (legacy) return legacy.value; } return 'viewer'; }; const onSubmit = async (data: InviteFormData) => { try { const payload: any = { email: data.email.trim(), name: data.name.trim(), password: data.password, is_active: true, }; if (data.role_id.startsWith('role_id:')) { payload.role_id = data.role_id.substring('role_id:'.length); payload.role = 'viewer'; } else { payload.role = data.role_id || 'viewer'; } await createUserMutation.mutateAsync(payload); toast.success(t('settings.userInvited')); reset({ name: '', email: '', password: '', role_id: '' }); setInviteOpen(false); } catch (err: unknown) { const errObj = asError(err); toast.error(errObj.message || t('common.error')); } }; const handleRoleChange = async (userId: string, newRoleValue: string) => { try { const data: any = {}; if (newRoleValue.startsWith('role_id:')) { data.role_id = newRoleValue.substring('role_id:'.length); data.role = 'viewer'; } else { data.role = newRoleValue; data.role_id = null; } await updateUserMutation.mutateAsync({ id: userId, data }); toast.success(t('settings.assignRole') + ' — OK'); } catch (err: unknown) { const errObj = asError(err); toast.error(errObj.message || t('common.error')); } }; const handleToggleActive = async () => { if (!confirmDeactivate) return; try { if (confirmDeactivate.is_active === false) { await updateUserMutation.mutateAsync({ id: confirmDeactivate.id, data: { is_active: true } }); toast.success(t('settings.userActivated')); } else { await updateUserMutation.mutateAsync({ id: confirmDeactivate.id, data: { is_active: false } }); toast.success(t('settings.userDeactivated')); } setConfirmDeactivate(null); } catch (err: unknown) { const errObj = asError(err); toast.error(errObj.message || t('common.error')); } }; const handleDeleteUser = async () => { if (!confirmDelete) return; try { await deleteUserMutation.mutateAsync(confirmDelete.id); toast.success(t('settings.userDeleted')); setConfirmDelete(null); } catch (err: unknown) { const errObj = asError(err); toast.error(errObj.message || t('common.error')); } }; const errorMsg = (key: string | undefined) => { if (!key) return undefined; if (key === 'required') return t('validation.required'); if (key === 'invalidEmail') return t('validation.email'); if (key === 'passwordTooShort') return t('auth.passwordTooShort'); return key; }; if (isLoading) { return (
); } return (

{t('settings.users')}

{users.length === 0 ? ( setInviteOpen(true)}>{t('settings.inviteUser')}} /> ) : (
{users.map((user: any) => (

{user.name}

{user.email}

{user.is_active === false ? t('common.inactive') : t('common.active')}