Files
leocrm/frontend/src/pages/SettingsUsers.tsx
T
leocrm-bot 700b7a71ad T07b: frontend feature pages — companies + contacts + settings + audit + dashboard + search
- 11 new feature pages (CompaniesList/Detail/Form, ContactsList/Detail/Form,
  SettingsProfile/Roles/Users, AuditLog, GlobalSearchResults)
- 3 page updates (Dashboard with StatCard+ActivityFeed, Settings with tree nav+Outlet,
  TopBar with SearchDropdown)
- 13 new routes in routes/index.tsx
- i18n updates (de.json + en.json) with companies/contacts/settings/audit/search keys
- 12 new test files + 2 existing test fixes (TopBar, AppShell)
- 7 shared components (DataGrid, Tabs, SearchDropdown, CsvImportDialog, StatCard,
  ActivityFeed, UnsavedChangesGuard)
- 16 new API hooks in hooks.ts
- Verification: 141 tests pass, build succeeds, tsc --noEmit clean
2026-06-29 11:01:39 +02:00

199 lines
7.3 KiB
TypeScript

import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useUsers, useCreateUser, useUpdateUser, useDeleteUser } 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 ROLES = [
{ value: 'admin', label: 'Administrator' },
{ value: 'manager', label: 'Manager' },
{ value: 'user', label: 'Mitarbeiter' },
{ value: 'guest', label: 'Gast' },
];
export function SettingsUsersPage() {
const { t } = useTranslation();
const toast = useToast();
const { data, isLoading } = useUsers(1, 100);
const createUserMutation = useCreateUser();
const updateUserMutation = useUpdateUser();
const deleteUserMutation = useDeleteUser();
const [inviteOpen, setInviteOpen] = useState(false);
const [inviteEmail, setInviteEmail] = useState('');
const [inviteRole, setInviteRole] = useState('user');
const [confirmDeactivate, setConfirmDeactivate] = useState<any>(null);
const users = data?.items ?? [];
const handleInvite = async () => {
if (!inviteEmail.trim()) {
toast.error(t('validation.required'));
return;
}
try {
await createUserMutation.mutateAsync({
email: inviteEmail.trim(),
role: inviteRole,
first_name: '',
last_name: '',
});
toast.success(t('settings.userInvited'));
setInviteEmail('');
setInviteRole('user');
setInviteOpen(false);
} catch (err: any) {
toast.error(err.message || t('common.error'));
}
};
const handleRoleChange = async (userId: string, newRole: string) => {
try {
await updateUserMutation.mutateAsync({ id: userId, data: { role: newRole } });
toast.success(t('settings.assignRole') + ' — OK');
} catch (err: any) {
toast.error(err.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: any) {
toast.error(err.message || t('common.error'));
}
};
if (isLoading) {
return (
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-users-page">
<Skeleton className="h-8 w-48 mb-6" />
<div className="space-y-3">
<Skeleton className="h-16" />
<Skeleton className="h-16" />
<Skeleton className="h-16" />
</div>
</div>
);
}
return (
<div className="p-6 max-w-4xl mx-auto" data-testid="settings-users-page">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-secondary-900">{t('settings.users')}</h1>
<Button onClick={() => setInviteOpen(true)} data-testid="invite-user-btn">
{t('settings.inviteUser')}
</Button>
</div>
{users.length === 0 ? (
<EmptyState
title={t('settings.noUsers')}
action={<Button onClick={() => setInviteOpen(true)}>{t('settings.inviteUser')}</Button>}
/>
) : (
<div className="space-y-3">
{users.map((user: any) => (
<Card key={user.id}>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3 flex-1 min-w-0">
<Avatar
name={`${user.first_name || ''} ${user.last_name || ''}`.trim() || user.email}
src={user.avatar_url}
size="sm"
/>
<div className="flex-1 min-w-0">
<p className="font-medium text-secondary-900 truncate">
{user.first_name} {user.last_name}
</p>
<p className="text-sm text-secondary-500 truncate">{user.email}</p>
</div>
<Badge variant={user.is_active === false ? 'danger' : 'success'}>
{user.is_active === false ? t('common.inactive') : t('common.active')}
</Badge>
</div>
<div className="flex items-center gap-2">
<Select
options={ROLES}
value={user.role || 'user'}
onChange={(e) => handleRoleChange(user.id, e.target.value)}
aria-label={t('settings.assignRole')}
className="w-36"
/>
<Button
variant={user.is_active === false ? 'primary' : 'danger'}
size="sm"
onClick={() => setConfirmDeactivate(user)}
data-testid={`toggle-active-${user.id}`}
>
{user.is_active === false ? t('settings.activate') : t('settings.deactivate')}
</Button>
</div>
</div>
</Card>
))}
</div>
)}
<Modal open={inviteOpen} onClose={() => setInviteOpen(false)} title={t('settings.inviteUser')}>
<div className="space-y-4" data-testid="invite-user-form">
<Input
label={t('settings.inviteEmail')}
type="email"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="neu.mitarbeiter@firma.de"
data-testid="invite-email"
/>
<Select
label={t('settings.inviteRole')}
options={ROLES}
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)}
/>
<div className="flex justify-end gap-3">
<Button variant="secondary" onClick={() => setInviteOpen(false)}>{t('common.cancel')}</Button>
<Button
onClick={handleInvite}
isLoading={createUserMutation.isPending}
data-testid="send-invite-btn"
>
{t('settings.inviteUser')}
</Button>
</div>
</div>
</Modal>
<ConfirmDialog
open={!!confirmDeactivate}
title={confirmDeactivate?.is_active === false ? t('settings.activate') : t('settings.deactivate')}
message={
confirmDeactivate?.is_active === false
? `${t('settings.activate')}: ${confirmDeactivate?.email}?`
: `${t('settings.deactivate')}: ${confirmDeactivate?.email}?`
}
variant={confirmDeactivate?.is_active === false ? 'primary' : 'danger'}
onConfirm={handleToggleActive}
onCancel={() => setConfirmDeactivate(null)}
/>
</div>
);
}