@micha.bigler/ui-core-micha 2.1.16 → 2.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/AuthContext.js +7 -14
- package/dist/auth/authApi.js +15 -0
- package/dist/components/AccessCodeManager.js +1 -1
- package/dist/components/BulkInviteCsvTab.js +1 -1
- package/dist/components/ProfileComponent.js +65 -5
- package/dist/components/UserInviteComponent.js +1 -1
- package/dist/components/UserListComponent.js +163 -21
- package/dist/i18n/authTranslations.js +30 -0
- package/dist/pages/AccountPage.js +9 -15
- package/package.json +1 -1
- package/src/auth/AuthContext.jsx +36 -15
- package/src/auth/authApi.jsx +17 -1
- package/src/components/AccessCodeManager.jsx +1 -1
- package/src/components/BulkInviteCsvTab.jsx +1 -1
- package/src/components/ProfileComponent.jsx +117 -4
- package/src/components/UserInviteComponent.jsx +2 -2
- package/src/components/UserListComponent.jsx +252 -40
- package/src/i18n/authTranslations.ts +31 -1
- package/src/pages/AccountPage.jsx +32 -31
|
@@ -2,7 +2,8 @@ import React, { useMemo, useState, useEffect } from 'react';
|
|
|
2
2
|
import {
|
|
3
3
|
Box, Typography, Table, TableBody, TableCell, TableContainer,
|
|
4
4
|
TableHead, TableRow, Paper, FormControl, Select,
|
|
5
|
-
MenuItem, Button,
|
|
5
|
+
MenuItem, Button, Tooltip, CircularProgress, Alert,
|
|
6
|
+
TableSortLabel, TextField
|
|
6
7
|
} from '@mui/material';
|
|
7
8
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
8
9
|
import { useTranslation } from 'react-i18next';
|
|
@@ -24,6 +25,11 @@ export function UserListComponent({
|
|
|
24
25
|
const [loading, setLoading] = useState(true);
|
|
25
26
|
const [error, setError] = useState(null);
|
|
26
27
|
const [rowActionLoading, setRowActionLoading] = useState({});
|
|
28
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
29
|
+
const [sortConfig, setSortConfig] = useState({ key: 'id', direction: 'asc' });
|
|
30
|
+
const cellSx = { verticalAlign: 'middle' };
|
|
31
|
+
const controlSx = { minWidth: 140 };
|
|
32
|
+
const actionButtonSx = { textTransform: 'none', minWidth: 90 };
|
|
27
33
|
|
|
28
34
|
const loadUsers = async () => {
|
|
29
35
|
setLoading(true);
|
|
@@ -32,7 +38,14 @@ export function UserListComponent({
|
|
|
32
38
|
// FIX: Removed apiUrl parameter.
|
|
33
39
|
// fetchUsersList uses USERS_BASE from authConfig internally.
|
|
34
40
|
const data = await fetchUsersList();
|
|
35
|
-
|
|
41
|
+
const list = Array.isArray(data) ? data : (Array.isArray(data?.results) ? data.results : []);
|
|
42
|
+
// Keep row identity stable even if backend returns unordered results.
|
|
43
|
+
list.sort((a, b) => {
|
|
44
|
+
const aId = Number(a?.id ?? 0);
|
|
45
|
+
const bId = Number(b?.id ?? 0);
|
|
46
|
+
return aId - bId;
|
|
47
|
+
});
|
|
48
|
+
setUsers(list);
|
|
36
49
|
} catch (err) {
|
|
37
50
|
setError(err.code || 'Auth.USER_LIST_FAILED');
|
|
38
51
|
} finally {
|
|
@@ -60,7 +73,7 @@ export function UserListComponent({
|
|
|
60
73
|
try {
|
|
61
74
|
// FIX: Removed apiUrl parameter.
|
|
62
75
|
await updateUserRole(userId, newRole);
|
|
63
|
-
loadUsers();
|
|
76
|
+
await loadUsers();
|
|
64
77
|
} catch (err) {
|
|
65
78
|
alert(t(err.code || 'Auth.USER_ROLE_UPDATE_FAILED'));
|
|
66
79
|
}
|
|
@@ -70,7 +83,7 @@ export function UserListComponent({
|
|
|
70
83
|
try {
|
|
71
84
|
// FIX: Removed apiUrl parameter.
|
|
72
85
|
await updateUserSupportStatus(userId, newValue);
|
|
73
|
-
loadUsers();
|
|
86
|
+
await loadUsers();
|
|
74
87
|
} catch (err) {
|
|
75
88
|
alert(t(err.code || 'Auth.USER_UPDATE_FAILED'));
|
|
76
89
|
}
|
|
@@ -122,6 +135,141 @@ export function UserListComponent({
|
|
|
122
135
|
[extraRowActions, listContext],
|
|
123
136
|
);
|
|
124
137
|
|
|
138
|
+
const getUserDisplayName = (user) => {
|
|
139
|
+
if (user?.first_name || user?.last_name) {
|
|
140
|
+
return `${user.first_name || ''} ${user.last_name || ''}`.trim();
|
|
141
|
+
}
|
|
142
|
+
return user?.username || '';
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const getExtraColumnSortValue = (column, user) => {
|
|
146
|
+
const valueContext = { user, currentUser, extraContext, t };
|
|
147
|
+
|
|
148
|
+
if (typeof column.getSortValue === 'function') {
|
|
149
|
+
return column.getSortValue(valueContext);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (typeof column.sortValue === 'function') {
|
|
153
|
+
return column.sortValue(valueContext);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (typeof column.getSearchValue === 'function') {
|
|
157
|
+
return column.getSearchValue(valueContext);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (user?.[column.key] !== undefined) return user[column.key];
|
|
161
|
+
if (user?.profile?.[column.key] !== undefined) return user.profile[column.key];
|
|
162
|
+
return '';
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const getExtraColumnSearchValue = (column, user) => {
|
|
166
|
+
const valueContext = { user, currentUser, extraContext, t };
|
|
167
|
+
|
|
168
|
+
if (typeof column.getSearchValue === 'function') {
|
|
169
|
+
return column.getSearchValue(valueContext);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return getExtraColumnSortValue(column, user);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const normalizeSortValue = (value) => {
|
|
176
|
+
if (value === null || value === undefined) return '';
|
|
177
|
+
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
178
|
+
if (typeof value === 'number') return value;
|
|
179
|
+
if (value instanceof Date) return value.getTime();
|
|
180
|
+
return String(value).toLocaleLowerCase();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const compareSortValues = (a, b) => {
|
|
184
|
+
const av = normalizeSortValue(a);
|
|
185
|
+
const bv = normalizeSortValue(b);
|
|
186
|
+
|
|
187
|
+
if (typeof av === 'number' && typeof bv === 'number') {
|
|
188
|
+
return av - bv;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return String(av).localeCompare(String(bv), undefined, {
|
|
192
|
+
numeric: true,
|
|
193
|
+
sensitivity: 'base',
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const getSortValueByKey = (user, sortKey) => {
|
|
198
|
+
if (sortKey === 'id') return Number(user?.id ?? 0);
|
|
199
|
+
if (sortKey === 'email') return user?.email || '';
|
|
200
|
+
if (sortKey === 'name') return getUserDisplayName(user);
|
|
201
|
+
if (sortKey === 'role') return user?.role || '';
|
|
202
|
+
if (sortKey === 'is_support_agent') return Boolean(user?.is_support_agent);
|
|
203
|
+
|
|
204
|
+
if (sortKey.startsWith('extra:')) {
|
|
205
|
+
const extraKey = sortKey.slice('extra:'.length);
|
|
206
|
+
const column = visibleExtraColumns.find((col) => String(col.key) === extraKey);
|
|
207
|
+
if (!column) return '';
|
|
208
|
+
return getExtraColumnSortValue(column, user);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return user?.[sortKey] ?? '';
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const toSearchText = (value) => {
|
|
215
|
+
if (value === null || value === undefined) return '';
|
|
216
|
+
if (Array.isArray(value)) return value.map((item) => toSearchText(item)).join(' ');
|
|
217
|
+
if (typeof value === 'object') return Object.values(value).map((item) => toSearchText(item)).join(' ');
|
|
218
|
+
return String(value).toLocaleLowerCase();
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const getSearchBucketForUser = (user) => {
|
|
222
|
+
const baseValues = [
|
|
223
|
+
user?.id,
|
|
224
|
+
user?.email,
|
|
225
|
+
user?.username,
|
|
226
|
+
user?.first_name,
|
|
227
|
+
user?.last_name,
|
|
228
|
+
getUserDisplayName(user),
|
|
229
|
+
user?.role,
|
|
230
|
+
user?.language,
|
|
231
|
+
user?.is_support_agent ? t('Common.YES', 'Yes') : t('Common.NO', 'No'),
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const extraValues = visibleExtraColumns.map((column) => getExtraColumnSearchValue(column, user));
|
|
235
|
+
|
|
236
|
+
return [...baseValues, ...extraValues].map((value) => toSearchText(value)).filter(Boolean);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const normalizedSearch = searchQuery.trim().toLocaleLowerCase();
|
|
240
|
+
|
|
241
|
+
const filteredAndSortedUsers = useMemo(() => {
|
|
242
|
+
const filtered = users.filter((user) => {
|
|
243
|
+
if (!normalizedSearch) return true;
|
|
244
|
+
const bucket = getSearchBucketForUser(user);
|
|
245
|
+
return bucket.some((entry) => entry.includes(normalizedSearch));
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
249
|
+
const aValue = getSortValueByKey(a, sortConfig.key);
|
|
250
|
+
const bValue = getSortValueByKey(b, sortConfig.key);
|
|
251
|
+
const cmp = compareSortValues(aValue, bValue);
|
|
252
|
+
if (cmp !== 0) return sortConfig.direction === 'asc' ? cmp : -cmp;
|
|
253
|
+
|
|
254
|
+
// Stable fallback
|
|
255
|
+
return Number(a?.id ?? 0) - Number(b?.id ?? 0);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
return sorted;
|
|
259
|
+
}, [users, normalizedSearch, sortConfig, visibleExtraColumns, currentUser, extraContext, t]);
|
|
260
|
+
|
|
261
|
+
const handleSort = (columnKey) => {
|
|
262
|
+
setSortConfig((prev) => {
|
|
263
|
+
if (prev.key === columnKey) {
|
|
264
|
+
return {
|
|
265
|
+
key: columnKey,
|
|
266
|
+
direction: prev.direction === 'asc' ? 'desc' : 'asc',
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return { key: columnKey, direction: 'asc' };
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
|
|
125
273
|
const runRowAction = async (action, user) => {
|
|
126
274
|
const actionId = `${action.key}:${user.id}`;
|
|
127
275
|
setRowActionLoading((prev) => ({ ...prev, [actionId]: true }));
|
|
@@ -145,6 +293,17 @@ export function UserListComponent({
|
|
|
145
293
|
return (
|
|
146
294
|
<Box>
|
|
147
295
|
<Typography variant="h6" gutterBottom>{t('UserList.TITLE', 'All Users')}</Typography>
|
|
296
|
+
|
|
297
|
+
<Box sx={{ mb: 2, maxWidth: 420 }}>
|
|
298
|
+
<TextField
|
|
299
|
+
fullWidth
|
|
300
|
+
size="small"
|
|
301
|
+
label={t('Common.SEARCH', 'Search')}
|
|
302
|
+
placeholder={t('UserList.SEARCH_PLACEHOLDER', 'Search users...')}
|
|
303
|
+
value={searchQuery}
|
|
304
|
+
onChange={(event) => setSearchQuery(event.target.value)}
|
|
305
|
+
/>
|
|
306
|
+
</Box>
|
|
148
307
|
|
|
149
308
|
{error && <Alert severity="error" sx={{ mb: 2 }}>{t(error)}</Alert>}
|
|
150
309
|
|
|
@@ -157,51 +316,93 @@ export function UserListComponent({
|
|
|
157
316
|
<Table size="small">
|
|
158
317
|
<TableHead>
|
|
159
318
|
<TableRow>
|
|
160
|
-
<TableCell
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
319
|
+
<TableCell sx={cellSx} sortDirection={sortConfig.key === 'email' ? sortConfig.direction : false}>
|
|
320
|
+
<TableSortLabel
|
|
321
|
+
active={sortConfig.key === 'email'}
|
|
322
|
+
direction={sortConfig.key === 'email' ? sortConfig.direction : 'asc'}
|
|
323
|
+
onClick={() => handleSort('email')}
|
|
324
|
+
>
|
|
325
|
+
{t('Auth.EMAIL_LABEL', 'Email')}
|
|
326
|
+
</TableSortLabel>
|
|
327
|
+
</TableCell>
|
|
328
|
+
<TableCell sx={cellSx} sortDirection={sortConfig.key === 'name' ? sortConfig.direction : false}>
|
|
329
|
+
<TableSortLabel
|
|
330
|
+
active={sortConfig.key === 'name'}
|
|
331
|
+
direction={sortConfig.key === 'name' ? sortConfig.direction : 'asc'}
|
|
332
|
+
onClick={() => handleSort('name')}
|
|
333
|
+
>
|
|
334
|
+
{t('Profile.NAME_LABEL', 'Name')}
|
|
335
|
+
</TableSortLabel>
|
|
336
|
+
</TableCell>
|
|
337
|
+
<TableCell sx={cellSx} sortDirection={sortConfig.key === 'role' ? sortConfig.direction : false}>
|
|
338
|
+
<TableSortLabel
|
|
339
|
+
active={sortConfig.key === 'role'}
|
|
340
|
+
direction={sortConfig.key === 'role' ? sortConfig.direction : 'asc'}
|
|
341
|
+
onClick={() => handleSort('role')}
|
|
342
|
+
>
|
|
343
|
+
{t('UserList.ROLE', 'Role')}
|
|
344
|
+
</TableSortLabel>
|
|
345
|
+
</TableCell>
|
|
346
|
+
<TableCell sx={cellSx} sortDirection={sortConfig.key === 'is_support_agent' ? sortConfig.direction : false}>
|
|
347
|
+
<TableSortLabel
|
|
348
|
+
active={sortConfig.key === 'is_support_agent'}
|
|
349
|
+
direction={sortConfig.key === 'is_support_agent' ? sortConfig.direction : 'asc'}
|
|
350
|
+
onClick={() => handleSort('is_support_agent')}
|
|
351
|
+
>
|
|
352
|
+
{t('UserList.SUPPORTER', 'Support Agent')}
|
|
353
|
+
</TableSortLabel>
|
|
354
|
+
</TableCell>
|
|
164
355
|
{visibleExtraColumns.map((column) => (
|
|
165
|
-
<TableCell
|
|
166
|
-
{
|
|
356
|
+
<TableCell
|
|
357
|
+
key={column.key}
|
|
358
|
+
sx={cellSx}
|
|
359
|
+
align={column.align || 'left'}
|
|
360
|
+
sortDirection={sortConfig.key === `extra:${column.key}` ? sortConfig.direction : false}
|
|
361
|
+
>
|
|
362
|
+
<TableSortLabel
|
|
363
|
+
active={sortConfig.key === `extra:${column.key}`}
|
|
364
|
+
direction={sortConfig.key === `extra:${column.key}` ? sortConfig.direction : 'asc'}
|
|
365
|
+
onClick={() => handleSort(`extra:${column.key}`)}
|
|
366
|
+
>
|
|
367
|
+
{typeof column.label === 'function' ? column.label(listContext) : column.label}
|
|
368
|
+
</TableSortLabel>
|
|
167
369
|
</TableCell>
|
|
168
370
|
))}
|
|
169
|
-
<TableCell>{t('Common.ACTIONS', 'Actions')}</TableCell>
|
|
371
|
+
<TableCell sx={cellSx}>{t('Common.ACTIONS', 'Actions')}</TableCell>
|
|
170
372
|
</TableRow>
|
|
171
373
|
</TableHead>
|
|
172
374
|
<TableBody>
|
|
173
|
-
{
|
|
375
|
+
{filteredAndSortedUsers.map((u) => (
|
|
174
376
|
<TableRow key={u.id}>
|
|
175
|
-
<TableCell>{u.email}</TableCell>
|
|
176
|
-
<TableCell>
|
|
177
|
-
{u
|
|
377
|
+
<TableCell sx={cellSx}>{u.email}</TableCell>
|
|
378
|
+
<TableCell sx={cellSx}>
|
|
379
|
+
{getUserDisplayName(u)}
|
|
178
380
|
</TableCell>
|
|
179
|
-
<TableCell>
|
|
180
|
-
<FormControl size="small" fullWidth disabled={!canEdit(u)}>
|
|
381
|
+
<TableCell sx={cellSx}>
|
|
382
|
+
<FormControl size="small" fullWidth sx={controlSx} disabled={!canEdit(u)}>
|
|
181
383
|
<Select
|
|
182
384
|
value={u.role || 'none'}
|
|
183
385
|
onChange={(e) => handleChangeRole(u.id, e.target.value)}
|
|
184
|
-
variant="
|
|
185
|
-
disableUnderline
|
|
386
|
+
variant="outlined"
|
|
186
387
|
>
|
|
187
388
|
{roles.map(r => <MenuItem key={r} value={r}>{r}</MenuItem>)}
|
|
188
389
|
</Select>
|
|
189
390
|
</FormControl>
|
|
190
391
|
</TableCell>
|
|
191
|
-
<TableCell>
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
disabled={!canEdit(u)}
|
|
198
|
-
sx={{ textTransform: 'none' }}
|
|
392
|
+
<TableCell sx={cellSx}>
|
|
393
|
+
<FormControl size="small" fullWidth sx={controlSx} disabled={!canEdit(u)}>
|
|
394
|
+
<Select
|
|
395
|
+
value={u.is_support_agent ? 'yes' : 'no'}
|
|
396
|
+
onChange={(e) => handleToggleSupporter(u.id, e.target.value === 'yes')}
|
|
397
|
+
variant="outlined"
|
|
199
398
|
>
|
|
200
|
-
|
|
201
|
-
|
|
399
|
+
<MenuItem value="yes">{t('Common.YES', 'Yes')}</MenuItem>
|
|
400
|
+
<MenuItem value="no">{t('Common.NO', 'No')}</MenuItem>
|
|
401
|
+
</Select>
|
|
402
|
+
</FormControl>
|
|
202
403
|
</TableCell>
|
|
203
404
|
{visibleExtraColumns.map((column) => (
|
|
204
|
-
<TableCell key={`${column.key}-${u.id}`} align={column.align || 'left'}>
|
|
405
|
+
<TableCell key={`${column.key}-${u.id}`} sx={cellSx} align={column.align || 'left'}>
|
|
205
406
|
{column.renderCell({
|
|
206
407
|
user: u,
|
|
207
408
|
canEdit: canEdit(u),
|
|
@@ -212,7 +413,8 @@ export function UserListComponent({
|
|
|
212
413
|
})}
|
|
213
414
|
</TableCell>
|
|
214
415
|
))}
|
|
215
|
-
<TableCell>
|
|
416
|
+
<TableCell sx={cellSx}>
|
|
417
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }}>
|
|
216
418
|
{visibleRowActions.map((action) => {
|
|
217
419
|
const actionId = `${action.key}:${u.id}`;
|
|
218
420
|
const isBusy = Boolean(rowActionLoading[actionId]);
|
|
@@ -230,9 +432,10 @@ export function UserListComponent({
|
|
|
230
432
|
<Button
|
|
231
433
|
key={`${action.key}-${u.id}`}
|
|
232
434
|
size="small"
|
|
435
|
+
variant="outlined"
|
|
233
436
|
onClick={() => runRowAction(action, u)}
|
|
234
437
|
disabled={isBusy || isDisabled}
|
|
235
|
-
sx={
|
|
438
|
+
sx={actionButtonSx}
|
|
236
439
|
>
|
|
237
440
|
{typeof action.label === 'function'
|
|
238
441
|
? action.label({ user: u, t, currentUser, canEdit: canEdit(u) })
|
|
@@ -240,17 +443,26 @@ export function UserListComponent({
|
|
|
240
443
|
</Button>
|
|
241
444
|
);
|
|
242
445
|
})}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
<
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
446
|
+
<Tooltip title={t('Common.DELETE')}>
|
|
447
|
+
<span>
|
|
448
|
+
<Button
|
|
449
|
+
size="small"
|
|
450
|
+
variant="outlined"
|
|
451
|
+
color="error"
|
|
452
|
+
startIcon={<DeleteIcon />}
|
|
453
|
+
onClick={() => handleDelete(u.id)}
|
|
454
|
+
disabled={!canEdit(u)}
|
|
455
|
+
sx={actionButtonSx}
|
|
456
|
+
>
|
|
457
|
+
{t('Common.DELETE', 'Delete')}
|
|
458
|
+
</Button>
|
|
459
|
+
</span>
|
|
460
|
+
</Tooltip>
|
|
461
|
+
</Box>
|
|
250
462
|
</TableCell>
|
|
251
463
|
</TableRow>
|
|
252
464
|
))}
|
|
253
|
-
{
|
|
465
|
+
{filteredAndSortedUsers.length === 0 && (
|
|
254
466
|
<TableRow>
|
|
255
467
|
<TableCell colSpan={5 + visibleExtraColumns.length} align="center">
|
|
256
468
|
{t('UserList.NO_USERS', 'No users found.')}
|
|
@@ -855,6 +855,36 @@ export const authTranslations = {
|
|
|
855
855
|
"en": "I allow convenience cookies.",
|
|
856
856
|
"sw": "Ninaruhusu vidakuzi vya urahisi."
|
|
857
857
|
},
|
|
858
|
+
"Profile.PRIVACY_STATEMENT_TITLE": {
|
|
859
|
+
"de": "Datenschutzerklärung",
|
|
860
|
+
"fr": "Déclaration de confidentialité",
|
|
861
|
+
"en": "Privacy statement",
|
|
862
|
+
"sw": "Taarifa ya faragha"
|
|
863
|
+
},
|
|
864
|
+
"Profile.COOKIES_STATEMENT_TITLE": {
|
|
865
|
+
"de": "Cookie-Richtlinie",
|
|
866
|
+
"fr": "Politique relative aux cookies",
|
|
867
|
+
"en": "Cookie statement",
|
|
868
|
+
"sw": "Taarifa ya vidakuzi"
|
|
869
|
+
},
|
|
870
|
+
"Profile.STATEMENT_LOADING": {
|
|
871
|
+
"de": "Statement wird geladen...",
|
|
872
|
+
"fr": "Chargement du document...",
|
|
873
|
+
"en": "Loading statement...",
|
|
874
|
+
"sw": "Inapakia taarifa..."
|
|
875
|
+
},
|
|
876
|
+
"Profile.PRIVACY_STATEMENT_EMPTY": {
|
|
877
|
+
"de": "Die Datenschutzerklärung ist derzeit nicht verfügbar.",
|
|
878
|
+
"fr": "La déclaration de confidentialité n'est actuellement pas disponible.",
|
|
879
|
+
"en": "Privacy statement is currently unavailable.",
|
|
880
|
+
"sw": "Taarifa ya faragha haipatikani kwa sasa."
|
|
881
|
+
},
|
|
882
|
+
"Profile.COOKIES_STATEMENT_EMPTY": {
|
|
883
|
+
"de": "Die Cookie-Richtlinie ist derzeit nicht verfügbar.",
|
|
884
|
+
"fr": "La politique relative aux cookies n'est actuellement pas disponible.",
|
|
885
|
+
"en": "Cookie statement is currently unavailable.",
|
|
886
|
+
"sw": "Taarifa ya vidakuzi haipatikani kwa sasa."
|
|
887
|
+
},
|
|
858
888
|
"Profile.SAVE_BUTTON": {
|
|
859
889
|
"de": "Speichern",
|
|
860
890
|
"fr": "Enregistrer",
|
|
@@ -1326,4 +1356,4 @@ export const authTranslations = {
|
|
|
1326
1356
|
"en": "Loading…",
|
|
1327
1357
|
"sw": "Inapakia..."
|
|
1328
1358
|
}
|
|
1329
|
-
};
|
|
1359
|
+
};
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
Box,
|
|
8
8
|
Typography,
|
|
9
9
|
Alert,
|
|
10
|
-
CircularProgress
|
|
10
|
+
CircularProgress,
|
|
11
|
+
Paper,
|
|
12
|
+
Stack,
|
|
11
13
|
} from '@mui/material';
|
|
12
14
|
import { useTranslation } from 'react-i18next';
|
|
13
15
|
|
|
@@ -46,7 +48,10 @@ export function AccountPage({
|
|
|
46
48
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
47
49
|
|
|
48
50
|
// 1. URL State Management
|
|
49
|
-
const
|
|
51
|
+
const currentTabRaw = searchParams.get('tab') || 'profile';
|
|
52
|
+
const currentTab = ['invite', 'bulk-invite-csv', 'access'].includes(currentTabRaw)
|
|
53
|
+
? 'invite'
|
|
54
|
+
: currentTabRaw;
|
|
50
55
|
const fromRecovery = searchParams.get('from') === 'recovery';
|
|
51
56
|
const fromWeakLogin = searchParams.get('from') === 'weak_login';
|
|
52
57
|
|
|
@@ -82,18 +87,8 @@ export function AccountPage({
|
|
|
82
87
|
list.push({ value: 'users', label: t('Account.TAB_USERS', 'Users') });
|
|
83
88
|
}
|
|
84
89
|
|
|
85
|
-
if (isSuperUser || perms.can_invite) {
|
|
90
|
+
if (isSuperUser || perms.can_invite || perms.can_manage_access_codes) {
|
|
86
91
|
list.push({ value: 'invite', label: t('Account.TAB_INVITE', 'Invite') });
|
|
87
|
-
if (showBulkInviteCsvTab) {
|
|
88
|
-
list.push({
|
|
89
|
-
value: 'bulk-invite-csv',
|
|
90
|
-
label: t('Account.TAB_BULK_INVITE_CSV', 'Bulk Invite CSV'),
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (isSuperUser || perms.can_manage_access_codes) {
|
|
96
|
-
list.push({ value: 'access', label: t('Account.TAB_ACCESS_CODES', 'Access Codes') });
|
|
97
92
|
}
|
|
98
93
|
|
|
99
94
|
if (isSuperUser || perms.can_view_support) {
|
|
@@ -112,7 +107,7 @@ export function AccountPage({
|
|
|
112
107
|
});
|
|
113
108
|
|
|
114
109
|
return list;
|
|
115
|
-
}, [user, perms, t, isSuperUser,
|
|
110
|
+
}, [user, perms, t, isSuperUser, extraTabs]);
|
|
116
111
|
|
|
117
112
|
// 4. Loading & Auth Checks
|
|
118
113
|
if (loading) {
|
|
@@ -137,7 +132,7 @@ export function AccountPage({
|
|
|
137
132
|
const activeTabExists = tabs.some(t => t.value === currentTab);
|
|
138
133
|
// Falls der Tab nicht erlaubt ist (z.B. manuell in URL eingegeben), Fallback auf 'profile'
|
|
139
134
|
const safeTab = activeTabExists ? currentTab : 'profile';
|
|
140
|
-
const builtInTabValues = new Set(['profile', 'security', 'users', 'invite', '
|
|
135
|
+
const builtInTabValues = new Set(['profile', 'security', 'users', 'invite', 'support']);
|
|
141
136
|
const activeExtraTab = builtInTabValues.has(safeTab)
|
|
142
137
|
? null
|
|
143
138
|
: extraTabs.find((tab) => tab.value === safeTab);
|
|
@@ -198,22 +193,28 @@ export function AccountPage({
|
|
|
198
193
|
|
|
199
194
|
{safeTab === 'invite' && (
|
|
200
195
|
<Box sx={{ mt: 2 }}>
|
|
201
|
-
<
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
{
|
|
215
|
-
|
|
216
|
-
|
|
196
|
+
<Stack spacing={2.5}>
|
|
197
|
+
{(isSuperUser || perms.can_invite) && (
|
|
198
|
+
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
199
|
+
<UserInviteComponent />
|
|
200
|
+
</Paper>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{(isSuperUser || perms.can_invite) && showBulkInviteCsvTab && (
|
|
204
|
+
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
205
|
+
<BulkInviteCsvTab {...bulkInviteCsvProps} />
|
|
206
|
+
</Paper>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{(isSuperUser || perms.can_manage_access_codes) && (
|
|
210
|
+
<Paper variant="outlined" sx={{ p: 2.5, borderRadius: 2 }}>
|
|
211
|
+
<Typography variant="body2" sx={{ mb: 2, color: 'text.secondary' }}>
|
|
212
|
+
{t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.')}
|
|
213
|
+
</Typography>
|
|
214
|
+
<AccessCodeManager />
|
|
215
|
+
</Paper>
|
|
216
|
+
)}
|
|
217
|
+
</Stack>
|
|
217
218
|
</Box>
|
|
218
219
|
)}
|
|
219
220
|
|