178 lines
5.9 KiB
TypeScript
178 lines
5.9 KiB
TypeScript
import { useState, useMemo } from 'react';
|
|
import {
|
|
Box,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableContainer,
|
|
TableHead,
|
|
TableRow,
|
|
TableSortLabel,
|
|
Paper,
|
|
TextField,
|
|
Chip,
|
|
Typography,
|
|
CircularProgress,
|
|
} from '@mui/material';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { adminApi } from '../../services/admin';
|
|
import type { UserOverview } from '../../types/admin.types';
|
|
|
|
type SortKey = 'name' | 'email' | 'role' | 'is_active' | 'last_login_at';
|
|
type SortDir = 'asc' | 'desc';
|
|
|
|
function formatRelativeTime(dateStr: string | null): string {
|
|
if (!dateStr) return 'Nie';
|
|
const date = new Date(dateStr);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
if (diffMins < 1) return 'Gerade eben';
|
|
if (diffMins < 60) return `vor ${diffMins}m`;
|
|
const diffHours = Math.floor(diffMins / 60);
|
|
if (diffHours < 24) return `vor ${diffHours}h`;
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
return `vor ${diffDays}d`;
|
|
}
|
|
|
|
function UserOverviewTab() {
|
|
const [search, setSearch] = useState('');
|
|
const [sortKey, setSortKey] = useState<SortKey>('name');
|
|
const [sortDir, setSortDir] = useState<SortDir>('asc');
|
|
|
|
const { data: users, isLoading, isError } = useQuery({
|
|
queryKey: ['admin', 'users'],
|
|
queryFn: adminApi.getUsers,
|
|
});
|
|
|
|
const handleSort = (key: SortKey) => {
|
|
if (sortKey === key) {
|
|
setSortDir(sortDir === 'asc' ? 'desc' : 'asc');
|
|
} else {
|
|
setSortKey(key);
|
|
setSortDir('asc');
|
|
}
|
|
};
|
|
|
|
const filtered = useMemo(() => {
|
|
if (!users) return [];
|
|
const q = search.toLowerCase();
|
|
let result = users.filter(
|
|
(u) => u.name?.toLowerCase().includes(q) || u.email?.toLowerCase().includes(q)
|
|
);
|
|
|
|
result.sort((a, b) => {
|
|
const valA = a[sortKey];
|
|
const valB = b[sortKey];
|
|
let cmp = 0;
|
|
if (valA == null && valB == null) cmp = 0;
|
|
else if (valA == null) cmp = -1;
|
|
else if (valB == null) cmp = 1;
|
|
else if (typeof valA === 'string' && typeof valB === 'string') {
|
|
cmp = valA.localeCompare(valB);
|
|
} else if (typeof valA === 'boolean' && typeof valB === 'boolean') {
|
|
cmp = valA === valB ? 0 : valA ? 1 : -1;
|
|
}
|
|
return sortDir === 'asc' ? cmp : -cmp;
|
|
});
|
|
return result;
|
|
}, [users, search, sortKey, sortDir]);
|
|
|
|
if (isLoading) {
|
|
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
|
}
|
|
|
|
if (isError) {
|
|
return (
|
|
<Box sx={{ p: 4, textAlign: 'center' }}>
|
|
<Typography color="error" gutterBottom>Benutzerdaten konnten nicht geladen werden.</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Bitte versuchen Sie es später erneut.
|
|
</Typography>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
|
<Typography variant="h6">Benutzer ({filtered.length})</Typography>
|
|
<TextField
|
|
size="small"
|
|
placeholder="Suche nach Name oder E-Mail..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
sx={{ minWidth: 280 }}
|
|
/>
|
|
</Box>
|
|
|
|
<TableContainer component={Paper}>
|
|
<Table>
|
|
<TableHead>
|
|
<TableRow>
|
|
<TableCell>
|
|
<TableSortLabel active={sortKey === 'name'} direction={sortKey === 'name' ? sortDir : 'asc'} onClick={() => handleSort('name')}>
|
|
Name
|
|
</TableSortLabel>
|
|
</TableCell>
|
|
<TableCell>
|
|
<TableSortLabel active={sortKey === 'email'} direction={sortKey === 'email' ? sortDir : 'asc'} onClick={() => handleSort('email')}>
|
|
E-Mail
|
|
</TableSortLabel>
|
|
</TableCell>
|
|
<TableCell>
|
|
<TableSortLabel active={sortKey === 'role'} direction={sortKey === 'role' ? sortDir : 'asc'} onClick={() => handleSort('role')}>
|
|
Rolle
|
|
</TableSortLabel>
|
|
</TableCell>
|
|
<TableCell>Gruppen</TableCell>
|
|
<TableCell>
|
|
<TableSortLabel active={sortKey === 'is_active'} direction={sortKey === 'is_active' ? sortDir : 'asc'} onClick={() => handleSort('is_active')}>
|
|
Status
|
|
</TableSortLabel>
|
|
</TableCell>
|
|
<TableCell>
|
|
<TableSortLabel active={sortKey === 'last_login_at'} direction={sortKey === 'last_login_at' ? sortDir : 'asc'} onClick={() => handleSort('last_login_at')}>
|
|
Letzter Login
|
|
</TableSortLabel>
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableHead>
|
|
<TableBody>
|
|
{filtered.map((user: UserOverview) => (
|
|
<TableRow key={user.id}>
|
|
<TableCell>{user.name}</TableCell>
|
|
<TableCell>{user.email}</TableCell>
|
|
<TableCell>
|
|
<Chip
|
|
label={user.role}
|
|
size="small"
|
|
color={user.role === 'admin' ? 'error' : 'default'}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
|
{(user.groups ?? []).map((g) => (
|
|
<Chip key={g} label={g} size="small" variant="outlined" />
|
|
))}
|
|
</Box>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Chip
|
|
label={user.is_active ? 'Aktiv' : 'Inaktiv'}
|
|
size="small"
|
|
color={user.is_active ? 'success' : 'default'}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>{formatRelativeTime(user.last_login_at)}</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</TableContainer>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export default UserOverviewTab;
|