@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
package/dist/auth/AuthContext.js
CHANGED
|
@@ -7,6 +7,11 @@ export const AuthContext = createContext(null);
|
|
|
7
7
|
export const AuthProvider = ({ children }) => {
|
|
8
8
|
const [user, setUser] = useState(null);
|
|
9
9
|
const [loading, setLoading] = useState(true);
|
|
10
|
+
const mapUserFromApi = (data) => {
|
|
11
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
12
|
+
const profile = (data === null || data === void 0 ? void 0 : data.profile) || {};
|
|
13
|
+
return Object.assign(Object.assign({}, data), { id: data === null || data === void 0 ? void 0 : data.id, username: data === null || data === void 0 ? void 0 : data.username, email: data === null || data === void 0 ? void 0 : data.email, first_name: data === null || data === void 0 ? void 0 : data.first_name, last_name: data === null || data === void 0 ? void 0 : data.last_name, role: (_b = (_a = data === null || data === void 0 ? void 0 : data.role) !== null && _a !== void 0 ? _a : profile === null || profile === void 0 ? void 0 : profile.role) !== null && _b !== void 0 ? _b : null, language: (_d = (_c = data === null || data === void 0 ? void 0 : data.language) !== null && _c !== void 0 ? _c : profile === null || profile === void 0 ? void 0 : profile.language) !== null && _d !== void 0 ? _d : 'en', is_superuser: Boolean(data === null || data === void 0 ? void 0 : data.is_superuser), is_new: Boolean((_e = data === null || data === void 0 ? void 0 : data.is_new) !== null && _e !== void 0 ? _e : profile === null || profile === void 0 ? void 0 : profile.is_new), is_invited: Boolean((_f = data === null || data === void 0 ? void 0 : data.is_invited) !== null && _f !== void 0 ? _f : profile === null || profile === void 0 ? void 0 : profile.is_invited), accepted_privacy_statement: Boolean((_g = data === null || data === void 0 ? void 0 : data.accepted_privacy_statement) !== null && _g !== void 0 ? _g : profile === null || profile === void 0 ? void 0 : profile.accepted_privacy_statement), accepted_convenience_cookies: Boolean((_h = data === null || data === void 0 ? void 0 : data.accepted_convenience_cookies) !== null && _h !== void 0 ? _h : profile === null || profile === void 0 ? void 0 : profile.accepted_convenience_cookies), is_support_agent: Boolean((_j = data === null || data === void 0 ? void 0 : data.is_support_agent) !== null && _j !== void 0 ? _j : profile === null || profile === void 0 ? void 0 : profile.is_support_agent), support_contact_id: (_l = (_k = data === null || data === void 0 ? void 0 : data.support_contact_id) !== null && _k !== void 0 ? _k : profile === null || profile === void 0 ? void 0 : profile.support_contact_id) !== null && _l !== void 0 ? _l : null, security_state: data === null || data === void 0 ? void 0 : data.security_state, available_roles: (data === null || data === void 0 ? void 0 : data.available_roles) || [], ui_permissions: (data === null || data === void 0 ? void 0 : data.ui_permissions) || {}, can_manage_support_agents: Boolean(data === null || data === void 0 ? void 0 : data.can_manage_support_agents), can_manage: Boolean(data === null || data === void 0 ? void 0 : data.can_manage), is_active: data === null || data === void 0 ? void 0 : data.is_active, last_login: data === null || data === void 0 ? void 0 : data.last_login, date_joined: data === null || data === void 0 ? void 0 : data.date_joined });
|
|
14
|
+
};
|
|
10
15
|
useEffect(() => {
|
|
11
16
|
let isMounted = true;
|
|
12
17
|
const initAuth = async () => {
|
|
@@ -16,19 +21,7 @@ export const AuthProvider = ({ children }) => {
|
|
|
16
21
|
// 2) Load user
|
|
17
22
|
const data = await fetchCurrentUser();
|
|
18
23
|
if (isMounted) {
|
|
19
|
-
|
|
20
|
-
setUser({
|
|
21
|
-
id: data.id,
|
|
22
|
-
username: data.username,
|
|
23
|
-
email: data.email,
|
|
24
|
-
first_name: data.first_name,
|
|
25
|
-
last_name: data.last_name,
|
|
26
|
-
role: data.role,
|
|
27
|
-
is_superuser: data.is_superuser,
|
|
28
|
-
security_state: data.security_state,
|
|
29
|
-
available_roles: data.available_roles,
|
|
30
|
-
ui_permissions: data.ui_permissions,
|
|
31
|
-
});
|
|
24
|
+
setUser(mapUserFromApi(data));
|
|
32
25
|
}
|
|
33
26
|
}
|
|
34
27
|
catch (err) {
|
|
@@ -45,7 +38,7 @@ export const AuthProvider = ({ children }) => {
|
|
|
45
38
|
return () => { isMounted = false; };
|
|
46
39
|
}, []);
|
|
47
40
|
const login = (userData) => {
|
|
48
|
-
setUser((prev) => (Object.assign(Object.assign({}, prev), userData)));
|
|
41
|
+
setUser((prev) => (Object.assign(Object.assign({}, prev), mapUserFromApi(userData))));
|
|
49
42
|
};
|
|
50
43
|
const logout = async () => {
|
|
51
44
|
try {
|
package/dist/auth/authApi.js
CHANGED
|
@@ -24,6 +24,21 @@ export async function updateUserProfile(data) {
|
|
|
24
24
|
throw normaliseApiError(error, 'Auth.PROFILE_UPDATE_FAILED');
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
function normalizeStatementText(data) {
|
|
28
|
+
if (typeof data === 'string')
|
|
29
|
+
return data;
|
|
30
|
+
if (data && typeof data.content === 'string')
|
|
31
|
+
return data.content;
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
export async function fetchPrivacyStatement() {
|
|
35
|
+
const res = await apiClient.get('/api/utils/privacy/');
|
|
36
|
+
return normalizeStatementText(res.data);
|
|
37
|
+
}
|
|
38
|
+
export async function fetchCookieStatement() {
|
|
39
|
+
const res = await apiClient.get('/api/utils/cookie/');
|
|
40
|
+
return normalizeStatementText(res.data);
|
|
41
|
+
}
|
|
27
42
|
export async function fetchHeadlessSession() {
|
|
28
43
|
const res = await apiClient.get(`${HEADLESS_BASE}/auth/session`);
|
|
29
44
|
return res.data;
|
|
@@ -90,7 +90,7 @@ export function AccessCodeManager() {
|
|
|
90
90
|
if (loading) {
|
|
91
91
|
return (_jsx(Box, { sx: { py: 3, display: 'flex', justifyContent: 'center' }, children: _jsx(CircularProgress, {}) }));
|
|
92
92
|
}
|
|
93
|
-
return (_jsxs(Box, {
|
|
93
|
+
return (_jsxs(Box, { children: [errorKey && (_jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(errorKey) })), successKey && (_jsx(Alert, { severity: "success", sx: { mb: 2 }, children: t(successKey) })), _jsxs(Box, { sx: { mb: 3 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Auth.ACCESS_CODE_SECTION_ACTIVE') }), codes.length === 0 ? (_jsx(Typography, { variant: "body2", children: t('Auth.ACCESS_CODE_NONE') })) : (_jsx(Stack, { direction: "row", flexWrap: "wrap", gap: 1, children: codes.map((code) => (_jsx(Chip, { label: code.code, onDelete: () => handleDelete(code.id), deleteIcon: _jsx(CloseIcon, {}) }, code.id))) }))] }), _jsxs(Box, { sx: { mb: 3 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Auth.ACCESS_CODE_SECTION_GENERATE') }), _jsxs(Box, { sx: { maxWidth: 360 }, children: [_jsx(Typography, { variant: "body2", gutterBottom: true, children: t('Auth.ACCESS_CODE_LENGTH_LABEL', { length }) }), _jsx(Slider, { min: 6, max: 32, step: 1, value: length, onChange: (_, val) => setLength(val), valueLabelDisplay: "auto", disabled: submitting })] }), _jsx(Button, { variant: "contained", sx: { mt: 1 }, onClick: handleGenerateClick, disabled: submitting, children: submitting
|
|
94
94
|
? t('Auth.SAVE_BUTTON_LOADING')
|
|
95
95
|
: t('Auth.ACCESS_CODE_GENERATE_BUTTON') })] }), _jsxs(Box, { sx: { mb: 2, maxWidth: 360 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Auth.ACCESS_CODE_SECTION_MANUAL') }), _jsxs(Box, { sx: { display: 'flex', gap: 1 }, children: [_jsx(TextField, { label: t('Auth.ACCESS_CODE_LABEL'), fullWidth: true, value: manualCode, onChange: (e) => setManualCode(e.target.value), disabled: submitting }), _jsx(Button, { variant: "outlined", onClick: handleAddManual, disabled: submitting, children: t('Auth.ACCESS_CODE_ADD_BUTTON') })] })] })] }));
|
|
96
96
|
}
|
|
@@ -89,7 +89,7 @@ export function BulkInviteCsvTab({ inviteFn = (email) => requestInviteWithCode(e
|
|
|
89
89
|
if (onCompleted)
|
|
90
90
|
onCompleted(nextResults);
|
|
91
91
|
};
|
|
92
|
-
return (_jsxs(Box, {
|
|
92
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Account.BULK_INVITE_TITLE', 'Bulk Invite via CSV') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.BULK_INVITE_HINT', 'Upload a CSV file containing email addresses. Header "email" is supported.') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), success && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: success }), _jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'center', mb: 2, flexWrap: 'wrap' }, children: [_jsxs(Button, { variant: "outlined", component: "label", disabled: busy, children: [t('Account.BULK_INVITE_UPLOAD', 'Upload CSV'), _jsx("input", { type: "file", accept: ".csv,text/csv", hidden: true, onChange: handleFile })] }), _jsx(Button, { variant: "contained", onClick: handleInviteAll, disabled: busy || emails.length === 0, children: t('Account.BULK_INVITE_SEND', 'Send Invites') }), _jsx(Typography, { variant: "body2", children: t('Account.BULK_INVITE_COUNT', '{{count}} emails loaded', { count: emails.length }) })] }), busy && (_jsx(Box, { sx: { mb: 2 }, children: _jsx(LinearProgress, { variant: "determinate", value: progress }) })), emails.length > 0 && (_jsx(TableContainer, { component: Paper, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('Auth.EMAIL_LABEL', 'Email') }), _jsx(TableCell, { children: t('Common.STATUS', 'Status') }), _jsx(TableCell, { children: t('Common.DETAILS', 'Details') })] }) }), _jsx(TableBody, { children: emails.map((email) => {
|
|
93
93
|
const row = results[email];
|
|
94
94
|
return (_jsxs(TableRow, { children: [_jsx(TableCell, { children: email }), _jsx(TableCell, { children: row ? (row.ok ? t('Common.SUCCESS', 'Success') : t('Common.ERROR', 'Error')) : '-' }), _jsx(TableCell, { children: (row === null || row === void 0 ? void 0 : row.message) || '-' })] }, email));
|
|
95
95
|
}) })] }) })), done > 0 && (_jsxs(Typography, { variant: "body2", sx: { mt: 2 }, children: [t('Account.BULK_INVITE_PROGRESS', '{{done}} / {{total}} processed', { done, total }), ' - ', t('Account.BULK_INVITE_SUCCESS_COUNT', '{{count}} successful', { count: successCount })] }))] }));
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// src/components/ProfileComponent.jsx
|
|
3
3
|
import React, { useEffect, useState, useContext } from 'react';
|
|
4
|
-
import { Box, Stack, TextField, FormControlLabel, Checkbox, Button, CircularProgress, Alert, Typography, } from '@mui/material';
|
|
4
|
+
import { Box, Stack, TextField, FormControlLabel, Checkbox, Button, CircularProgress, Alert, Typography, Accordion, AccordionSummary, AccordionDetails, } from '@mui/material';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
// WICHTIG: Importiere den Context, um die bereits geladenen Daten zu nutzen
|
|
7
7
|
import { AuthContext } from '../auth/AuthContext';
|
|
8
|
-
|
|
8
|
+
import { fetchCookieStatement, fetchPrivacyStatement } from '../auth/authApi';
|
|
9
|
+
export function ProfileComponent({ onSubmit, submitText, showName = true, showPrivacy = true, showCookies = true, showStatements = true, privacyStatementText = null, cookieStatementText = null, }) {
|
|
9
10
|
const { t } = useTranslation();
|
|
10
11
|
// WICHTIG: Wir holen den User direkt aus dem globalen State
|
|
11
12
|
// Das verhindert den doppelten Request und den ReferenceError
|
|
@@ -20,6 +21,10 @@ export function ProfileComponent({ onSubmit, submitText, showName = true, showPr
|
|
|
20
21
|
const [lastName, setLastName] = useState('');
|
|
21
22
|
const [acceptedPrivacy, setAcceptedPrivacy] = useState(false);
|
|
22
23
|
const [acceptedCookies, setAcceptedCookies] = useState(false);
|
|
24
|
+
const [privacyStatement, setPrivacyStatement] = useState(privacyStatementText || '');
|
|
25
|
+
const [cookieStatement, setCookieStatement] = useState(cookieStatementText || '');
|
|
26
|
+
const [loadingPrivacyStatement, setLoadingPrivacyStatement] = useState(false);
|
|
27
|
+
const [loadingCookieStatement, setLoadingCookieStatement] = useState(false);
|
|
23
28
|
// Synchronisiere Formular-Daten, sobald der User aus dem Context da ist
|
|
24
29
|
useEffect(() => {
|
|
25
30
|
var _a, _b, _c, _d;
|
|
@@ -32,6 +37,59 @@ export function ProfileComponent({ onSubmit, submitText, showName = true, showPr
|
|
|
32
37
|
setAcceptedCookies(Boolean(user.accepted_convenience_cookies));
|
|
33
38
|
}
|
|
34
39
|
}, [user]);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (privacyStatementText !== null && privacyStatementText !== undefined) {
|
|
42
|
+
setPrivacyStatement(String(privacyStatementText));
|
|
43
|
+
}
|
|
44
|
+
}, [privacyStatementText]);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (cookieStatementText !== null && cookieStatementText !== undefined) {
|
|
47
|
+
setCookieStatement(String(cookieStatementText));
|
|
48
|
+
}
|
|
49
|
+
}, [cookieStatementText]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
let cancelled = false;
|
|
52
|
+
const loadStatements = async () => {
|
|
53
|
+
if (!showStatements)
|
|
54
|
+
return;
|
|
55
|
+
if (showPrivacy && (privacyStatementText === null || privacyStatementText === undefined)) {
|
|
56
|
+
setLoadingPrivacyStatement(true);
|
|
57
|
+
try {
|
|
58
|
+
const text = await fetchPrivacyStatement();
|
|
59
|
+
if (!cancelled)
|
|
60
|
+
setPrivacyStatement(text || '');
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (!cancelled)
|
|
64
|
+
setPrivacyStatement('');
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
if (!cancelled)
|
|
68
|
+
setLoadingPrivacyStatement(false);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (showCookies && (cookieStatementText === null || cookieStatementText === undefined)) {
|
|
72
|
+
setLoadingCookieStatement(true);
|
|
73
|
+
try {
|
|
74
|
+
const text = await fetchCookieStatement();
|
|
75
|
+
if (!cancelled)
|
|
76
|
+
setCookieStatement(text || '');
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (!cancelled)
|
|
80
|
+
setCookieStatement('');
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (!cancelled)
|
|
84
|
+
setLoadingCookieStatement(false);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
loadStatements();
|
|
89
|
+
return () => {
|
|
90
|
+
cancelled = true;
|
|
91
|
+
};
|
|
92
|
+
}, [showStatements, showPrivacy, showCookies, privacyStatementText, cookieStatementText]);
|
|
35
93
|
const handleSubmit = async (event) => {
|
|
36
94
|
event.preventDefault();
|
|
37
95
|
if (!onSubmit)
|
|
@@ -39,12 +97,14 @@ export function ProfileComponent({ onSubmit, submitText, showName = true, showPr
|
|
|
39
97
|
setSaving(true);
|
|
40
98
|
setErrorKey(null);
|
|
41
99
|
setSuccessKey(null);
|
|
42
|
-
const payload = {
|
|
100
|
+
const payload = Object.assign(Object.assign(Object.assign({}, (showName && {
|
|
43
101
|
first_name: firstName,
|
|
44
102
|
last_name: lastName,
|
|
103
|
+
})), (showPrivacy && {
|
|
45
104
|
accepted_privacy_statement: acceptedPrivacy,
|
|
105
|
+
})), (showCookies && {
|
|
46
106
|
accepted_convenience_cookies: acceptedCookies,
|
|
47
|
-
};
|
|
107
|
+
}));
|
|
48
108
|
try {
|
|
49
109
|
await onSubmit(payload);
|
|
50
110
|
setSuccessKey('Profile.UPDATE_SUCCESS');
|
|
@@ -68,5 +128,5 @@ export function ProfileComponent({ onSubmit, submitText, showName = true, showPr
|
|
|
68
128
|
}
|
|
69
129
|
const submitLabel = submitText || t('Profile.SAVE_BUTTON');
|
|
70
130
|
const submitLabelLoading = t('Profile.SAVE_BUTTON_LOADING');
|
|
71
|
-
return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { maxWidth: 600, display: 'flex', flexDirection: 'column', gap: 2 }, children: [errorKey && (_jsx(Alert, { severity: "error", children: t(errorKey) })), successKey && (_jsx(Alert, { severity: "success", children: t(successKey) })), _jsxs(Stack, { spacing: 2, children: [_jsx(TextField, { label: t('Profile.USERNAME_LABEL'), value: username, fullWidth: true, disabled: true }), _jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", value: email, fullWidth: true, disabled: true })] }), showName && (_jsxs(Stack, { spacing: 2, direction: { xs: 'column', sm: 'row' }, children: [_jsx(TextField, { label: t('Profile.FIRST_NAME_LABEL'), value: firstName, onChange: (e) => setFirstName(e.target.value), fullWidth: true }), _jsx(TextField, { label: t('Profile.LAST_NAME_LABEL'), value: lastName, onChange: (e) => setLastName(e.target.value), fullWidth: true })] })), (showPrivacy || showCookies) && (_jsxs(Box, { sx: { mt: 1 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Profile.PRIVACY_SECTION_TITLE') }), _jsxs(Stack, { spacing: 1, children: [showPrivacy && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedPrivacy, onChange: (e) => setAcceptedPrivacy(e.target.checked) }), label: t('Profile.ACCEPT_PRIVACY_LABEL') })), showCookies && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedCookies, onChange: (e) => setAcceptedCookies(e.target.checked) }), label: t('Profile.ACCEPT_COOKIES_LABEL') }))] })] })), _jsx(Box, { sx: { mt: 2 }, children: _jsx(Button, { type: "submit", variant: "contained", disabled: saving, children: saving ? submitLabelLoading : submitLabel }) })] }));
|
|
131
|
+
return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { maxWidth: 600, display: 'flex', flexDirection: 'column', gap: 2 }, children: [errorKey && (_jsx(Alert, { severity: "error", children: t(errorKey) })), successKey && (_jsx(Alert, { severity: "success", children: t(successKey) })), _jsxs(Stack, { spacing: 2, children: [_jsx(TextField, { label: t('Profile.USERNAME_LABEL'), value: username, fullWidth: true, disabled: true }), _jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", value: email, fullWidth: true, disabled: true })] }), showName && (_jsxs(Stack, { spacing: 2, direction: { xs: 'column', sm: 'row' }, children: [_jsx(TextField, { label: t('Profile.FIRST_NAME_LABEL'), value: firstName, onChange: (e) => setFirstName(e.target.value), fullWidth: true }), _jsx(TextField, { label: t('Profile.LAST_NAME_LABEL'), value: lastName, onChange: (e) => setLastName(e.target.value), fullWidth: true })] })), (showPrivacy || showCookies) && (_jsxs(Box, { sx: { mt: 1 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Profile.PRIVACY_SECTION_TITLE') }), _jsxs(Stack, { spacing: 1, children: [showPrivacy && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedPrivacy, onChange: (e) => setAcceptedPrivacy(e.target.checked) }), label: t('Profile.ACCEPT_PRIVACY_LABEL') })), showCookies && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedCookies, onChange: (e) => setAcceptedCookies(e.target.checked) }), label: t('Profile.ACCEPT_COOKIES_LABEL') }))] }), showStatements && (_jsxs(Stack, { spacing: 1.25, sx: { mt: 1.5 }, children: [showPrivacy && (_jsxs(Accordion, { disableGutters: true, children: [_jsx(AccordionSummary, { children: _jsx(Typography, { variant: "body2", fontWeight: 600, children: t('Profile.PRIVACY_STATEMENT_TITLE', 'Privacy statement') }) }), _jsx(AccordionDetails, { children: loadingPrivacyStatement ? (_jsx(Typography, { variant: "body2", color: "text.secondary", children: t('Profile.STATEMENT_LOADING', 'Loading statement...') })) : (_jsx(Typography, { variant: "body2", sx: { whiteSpace: 'pre-wrap' }, children: privacyStatement || t('Profile.PRIVACY_STATEMENT_EMPTY', 'Privacy statement is currently unavailable.') })) })] })), showCookies && (_jsxs(Accordion, { disableGutters: true, children: [_jsx(AccordionSummary, { children: _jsx(Typography, { variant: "body2", fontWeight: 600, children: t('Profile.COOKIES_STATEMENT_TITLE', 'Cookie statement') }) }), _jsx(AccordionDetails, { children: loadingCookieStatement ? (_jsx(Typography, { variant: "body2", color: "text.secondary", children: t('Profile.STATEMENT_LOADING', 'Loading statement...') })) : (_jsx(Typography, { variant: "body2", sx: { whiteSpace: 'pre-wrap' }, children: cookieStatement || t('Profile.COOKIES_STATEMENT_EMPTY', 'Cookie statement is currently unavailable.') })) })] }))] }))] })), _jsx(Box, { sx: { mt: 2 }, children: _jsx(Button, { type: "submit", variant: "contained", disabled: saving, children: saving ? submitLabelLoading : submitLabel }) })] }));
|
|
72
132
|
}
|
|
@@ -32,7 +32,7 @@ export function UserInviteComponent() {
|
|
|
32
32
|
setLoading(false);
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
|
-
return (_jsxs(Box, { sx: { maxWidth: 600
|
|
35
|
+
return (_jsxs(Box, { sx: { maxWidth: 600 }, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.INVITE_TITLE', 'Invite a new user') }), message && _jsx(Alert, { severity: "success", sx: { mb: 2 }, children: message }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: error }), _jsxs(Box, { sx: { display: 'flex', gap: 2, alignItems: 'flex-start' }, children: [_jsx(TextField, { label: t('Auth.EMAIL_LABEL', 'Email address'), type: "email", variant: "outlined", fullWidth: true, size: "small", value: inviteEmail, onChange: (e) => setInviteEmail(e.target.value), disabled: loading, onKeyPress: (e) => {
|
|
36
36
|
if (e.key === 'Enter')
|
|
37
37
|
inviteUser();
|
|
38
38
|
} }), _jsx(Button, { variant: "contained", onClick: inviteUser, disabled: loading || !inviteEmail, sx: { minWidth: 100, height: 40 }, children: loading ? _jsx(CircularProgress, { size: 24, color: "inherit" }) : t('Auth.INVITE_BUTTON', 'Invite') })] })] }));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useMemo, useState, useEffect } from 'react';
|
|
3
|
-
import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, FormControl, Select, MenuItem, Button,
|
|
3
|
+
import { Box, Typography, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, FormControl, Select, MenuItem, Button, Tooltip, CircularProgress, Alert, TableSortLabel, TextField } from '@mui/material';
|
|
4
4
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { fetchUsersList, deleteUser, updateUserRole, updateUserSupportStatus } from '../auth/authApi';
|
|
@@ -11,6 +11,11 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
11
11
|
const [loading, setLoading] = useState(true);
|
|
12
12
|
const [error, setError] = useState(null);
|
|
13
13
|
const [rowActionLoading, setRowActionLoading] = useState({});
|
|
14
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
15
|
+
const [sortConfig, setSortConfig] = useState({ key: 'id', direction: 'asc' });
|
|
16
|
+
const cellSx = { verticalAlign: 'middle' };
|
|
17
|
+
const controlSx = { minWidth: 140 };
|
|
18
|
+
const actionButtonSx = { textTransform: 'none', minWidth: 90 };
|
|
14
19
|
const loadUsers = async () => {
|
|
15
20
|
setLoading(true);
|
|
16
21
|
setError(null);
|
|
@@ -18,7 +23,15 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
18
23
|
// FIX: Removed apiUrl parameter.
|
|
19
24
|
// fetchUsersList uses USERS_BASE from authConfig internally.
|
|
20
25
|
const data = await fetchUsersList();
|
|
21
|
-
|
|
26
|
+
const list = Array.isArray(data) ? data : (Array.isArray(data === null || data === void 0 ? void 0 : data.results) ? data.results : []);
|
|
27
|
+
// Keep row identity stable even if backend returns unordered results.
|
|
28
|
+
list.sort((a, b) => {
|
|
29
|
+
var _a, _b;
|
|
30
|
+
const aId = Number((_a = a === null || a === void 0 ? void 0 : a.id) !== null && _a !== void 0 ? _a : 0);
|
|
31
|
+
const bId = Number((_b = b === null || b === void 0 ? void 0 : b.id) !== null && _b !== void 0 ? _b : 0);
|
|
32
|
+
return aId - bId;
|
|
33
|
+
});
|
|
34
|
+
setUsers(list);
|
|
22
35
|
}
|
|
23
36
|
catch (err) {
|
|
24
37
|
setError(err.code || 'Auth.USER_LIST_FAILED');
|
|
@@ -47,7 +60,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
47
60
|
try {
|
|
48
61
|
// FIX: Removed apiUrl parameter.
|
|
49
62
|
await updateUserRole(userId, newRole);
|
|
50
|
-
loadUsers();
|
|
63
|
+
await loadUsers();
|
|
51
64
|
}
|
|
52
65
|
catch (err) {
|
|
53
66
|
alert(t(err.code || 'Auth.USER_ROLE_UPDATE_FAILED'));
|
|
@@ -57,7 +70,7 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
57
70
|
try {
|
|
58
71
|
// FIX: Removed apiUrl parameter.
|
|
59
72
|
await updateUserSupportStatus(userId, newValue);
|
|
60
|
-
loadUsers();
|
|
73
|
+
await loadUsers();
|
|
61
74
|
}
|
|
62
75
|
catch (err) {
|
|
63
76
|
alert(t(err.code || 'Auth.USER_UPDATE_FAILED'));
|
|
@@ -95,6 +108,135 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
95
108
|
}), [currentUser, extraContext, t]);
|
|
96
109
|
const visibleExtraColumns = useMemo(() => extraColumns.filter((column) => typeof column.visible === 'function' ? column.visible(listContext) : true), [extraColumns, listContext]);
|
|
97
110
|
const visibleRowActions = useMemo(() => extraRowActions.filter((action) => typeof action.visible === 'function' ? action.visible(listContext) : true), [extraRowActions, listContext]);
|
|
111
|
+
const getUserDisplayName = (user) => {
|
|
112
|
+
if ((user === null || user === void 0 ? void 0 : user.first_name) || (user === null || user === void 0 ? void 0 : user.last_name)) {
|
|
113
|
+
return `${user.first_name || ''} ${user.last_name || ''}`.trim();
|
|
114
|
+
}
|
|
115
|
+
return (user === null || user === void 0 ? void 0 : user.username) || '';
|
|
116
|
+
};
|
|
117
|
+
const getExtraColumnSortValue = (column, user) => {
|
|
118
|
+
var _a;
|
|
119
|
+
const valueContext = { user, currentUser, extraContext, t };
|
|
120
|
+
if (typeof column.getSortValue === 'function') {
|
|
121
|
+
return column.getSortValue(valueContext);
|
|
122
|
+
}
|
|
123
|
+
if (typeof column.sortValue === 'function') {
|
|
124
|
+
return column.sortValue(valueContext);
|
|
125
|
+
}
|
|
126
|
+
if (typeof column.getSearchValue === 'function') {
|
|
127
|
+
return column.getSearchValue(valueContext);
|
|
128
|
+
}
|
|
129
|
+
if ((user === null || user === void 0 ? void 0 : user[column.key]) !== undefined)
|
|
130
|
+
return user[column.key];
|
|
131
|
+
if (((_a = user === null || user === void 0 ? void 0 : user.profile) === null || _a === void 0 ? void 0 : _a[column.key]) !== undefined)
|
|
132
|
+
return user.profile[column.key];
|
|
133
|
+
return '';
|
|
134
|
+
};
|
|
135
|
+
const getExtraColumnSearchValue = (column, user) => {
|
|
136
|
+
const valueContext = { user, currentUser, extraContext, t };
|
|
137
|
+
if (typeof column.getSearchValue === 'function') {
|
|
138
|
+
return column.getSearchValue(valueContext);
|
|
139
|
+
}
|
|
140
|
+
return getExtraColumnSortValue(column, user);
|
|
141
|
+
};
|
|
142
|
+
const normalizeSortValue = (value) => {
|
|
143
|
+
if (value === null || value === undefined)
|
|
144
|
+
return '';
|
|
145
|
+
if (typeof value === 'boolean')
|
|
146
|
+
return value ? 1 : 0;
|
|
147
|
+
if (typeof value === 'number')
|
|
148
|
+
return value;
|
|
149
|
+
if (value instanceof Date)
|
|
150
|
+
return value.getTime();
|
|
151
|
+
return String(value).toLocaleLowerCase();
|
|
152
|
+
};
|
|
153
|
+
const compareSortValues = (a, b) => {
|
|
154
|
+
const av = normalizeSortValue(a);
|
|
155
|
+
const bv = normalizeSortValue(b);
|
|
156
|
+
if (typeof av === 'number' && typeof bv === 'number') {
|
|
157
|
+
return av - bv;
|
|
158
|
+
}
|
|
159
|
+
return String(av).localeCompare(String(bv), undefined, {
|
|
160
|
+
numeric: true,
|
|
161
|
+
sensitivity: 'base',
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
const getSortValueByKey = (user, sortKey) => {
|
|
165
|
+
var _a, _b;
|
|
166
|
+
if (sortKey === 'id')
|
|
167
|
+
return Number((_a = user === null || user === void 0 ? void 0 : user.id) !== null && _a !== void 0 ? _a : 0);
|
|
168
|
+
if (sortKey === 'email')
|
|
169
|
+
return (user === null || user === void 0 ? void 0 : user.email) || '';
|
|
170
|
+
if (sortKey === 'name')
|
|
171
|
+
return getUserDisplayName(user);
|
|
172
|
+
if (sortKey === 'role')
|
|
173
|
+
return (user === null || user === void 0 ? void 0 : user.role) || '';
|
|
174
|
+
if (sortKey === 'is_support_agent')
|
|
175
|
+
return Boolean(user === null || user === void 0 ? void 0 : user.is_support_agent);
|
|
176
|
+
if (sortKey.startsWith('extra:')) {
|
|
177
|
+
const extraKey = sortKey.slice('extra:'.length);
|
|
178
|
+
const column = visibleExtraColumns.find((col) => String(col.key) === extraKey);
|
|
179
|
+
if (!column)
|
|
180
|
+
return '';
|
|
181
|
+
return getExtraColumnSortValue(column, user);
|
|
182
|
+
}
|
|
183
|
+
return (_b = user === null || user === void 0 ? void 0 : user[sortKey]) !== null && _b !== void 0 ? _b : '';
|
|
184
|
+
};
|
|
185
|
+
const toSearchText = (value) => {
|
|
186
|
+
if (value === null || value === undefined)
|
|
187
|
+
return '';
|
|
188
|
+
if (Array.isArray(value))
|
|
189
|
+
return value.map((item) => toSearchText(item)).join(' ');
|
|
190
|
+
if (typeof value === 'object')
|
|
191
|
+
return Object.values(value).map((item) => toSearchText(item)).join(' ');
|
|
192
|
+
return String(value).toLocaleLowerCase();
|
|
193
|
+
};
|
|
194
|
+
const getSearchBucketForUser = (user) => {
|
|
195
|
+
const baseValues = [
|
|
196
|
+
user === null || user === void 0 ? void 0 : user.id,
|
|
197
|
+
user === null || user === void 0 ? void 0 : user.email,
|
|
198
|
+
user === null || user === void 0 ? void 0 : user.username,
|
|
199
|
+
user === null || user === void 0 ? void 0 : user.first_name,
|
|
200
|
+
user === null || user === void 0 ? void 0 : user.last_name,
|
|
201
|
+
getUserDisplayName(user),
|
|
202
|
+
user === null || user === void 0 ? void 0 : user.role,
|
|
203
|
+
user === null || user === void 0 ? void 0 : user.language,
|
|
204
|
+
(user === null || user === void 0 ? void 0 : user.is_support_agent) ? t('Common.YES', 'Yes') : t('Common.NO', 'No'),
|
|
205
|
+
];
|
|
206
|
+
const extraValues = visibleExtraColumns.map((column) => getExtraColumnSearchValue(column, user));
|
|
207
|
+
return [...baseValues, ...extraValues].map((value) => toSearchText(value)).filter(Boolean);
|
|
208
|
+
};
|
|
209
|
+
const normalizedSearch = searchQuery.trim().toLocaleLowerCase();
|
|
210
|
+
const filteredAndSortedUsers = useMemo(() => {
|
|
211
|
+
const filtered = users.filter((user) => {
|
|
212
|
+
if (!normalizedSearch)
|
|
213
|
+
return true;
|
|
214
|
+
const bucket = getSearchBucketForUser(user);
|
|
215
|
+
return bucket.some((entry) => entry.includes(normalizedSearch));
|
|
216
|
+
});
|
|
217
|
+
const sorted = [...filtered].sort((a, b) => {
|
|
218
|
+
var _a, _b;
|
|
219
|
+
const aValue = getSortValueByKey(a, sortConfig.key);
|
|
220
|
+
const bValue = getSortValueByKey(b, sortConfig.key);
|
|
221
|
+
const cmp = compareSortValues(aValue, bValue);
|
|
222
|
+
if (cmp !== 0)
|
|
223
|
+
return sortConfig.direction === 'asc' ? cmp : -cmp;
|
|
224
|
+
// Stable fallback
|
|
225
|
+
return Number((_a = a === null || a === void 0 ? void 0 : a.id) !== null && _a !== void 0 ? _a : 0) - Number((_b = b === null || b === void 0 ? void 0 : b.id) !== null && _b !== void 0 ? _b : 0);
|
|
226
|
+
});
|
|
227
|
+
return sorted;
|
|
228
|
+
}, [users, normalizedSearch, sortConfig, visibleExtraColumns, currentUser, extraContext, t]);
|
|
229
|
+
const handleSort = (columnKey) => {
|
|
230
|
+
setSortConfig((prev) => {
|
|
231
|
+
if (prev.key === columnKey) {
|
|
232
|
+
return {
|
|
233
|
+
key: columnKey,
|
|
234
|
+
direction: prev.direction === 'asc' ? 'desc' : 'asc',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return { key: columnKey, direction: 'asc' };
|
|
238
|
+
});
|
|
239
|
+
};
|
|
98
240
|
const runRowAction = async (action, user) => {
|
|
99
241
|
const actionId = `${action.key}:${user.id}`;
|
|
100
242
|
setRowActionLoading((prev) => (Object.assign(Object.assign({}, prev), { [actionId]: true })));
|
|
@@ -116,27 +258,27 @@ export function UserListComponent({ roles = DEFAULT_ROLES, currentUser, extraCol
|
|
|
116
258
|
setRowActionLoading((prev) => (Object.assign(Object.assign({}, prev), { [actionId]: false })));
|
|
117
259
|
}
|
|
118
260
|
};
|
|
119
|
-
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('UserList.TITLE', 'All Users') }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(error) }), loading ? (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 3 }, children: _jsx(CircularProgress, {}) })) : (_jsx(TableContainer, { component: Paper, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { children: t('Auth.EMAIL_LABEL', 'Email') }), _jsx(TableCell, { children: t('Profile.NAME_LABEL', 'Name') }), _jsx(TableCell, { children: t('UserList.ROLE', 'Role') }), _jsx(TableCell, { children: t('UserList.SUPPORTER', 'Support Agent') }), visibleExtraColumns.map((column) => (_jsx(TableCell, { align: column.align || 'left', children: typeof column.label === 'function' ? column.label(listContext) : column.label }, column.key))), _jsx(TableCell, { children: t('Common.ACTIONS', 'Actions') })] }) }), _jsxs(TableBody, { children: [
|
|
261
|
+
return (_jsxs(Box, { children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('UserList.TITLE', 'All Users') }), _jsx(Box, { sx: { mb: 2, maxWidth: 420 }, children: _jsx(TextField, { fullWidth: true, size: "small", label: t('Common.SEARCH', 'Search'), placeholder: t('UserList.SEARCH_PLACEHOLDER', 'Search users...'), value: searchQuery, onChange: (event) => setSearchQuery(event.target.value) }) }), error && _jsx(Alert, { severity: "error", sx: { mb: 2 }, children: t(error) }), loading ? (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', p: 3 }, children: _jsx(CircularProgress, {}) })) : (_jsx(TableContainer, { component: Paper, children: _jsxs(Table, { size: "small", children: [_jsx(TableHead, { children: _jsxs(TableRow, { children: [_jsx(TableCell, { sx: cellSx, sortDirection: sortConfig.key === 'email' ? sortConfig.direction : false, children: _jsx(TableSortLabel, { active: sortConfig.key === 'email', direction: sortConfig.key === 'email' ? sortConfig.direction : 'asc', onClick: () => handleSort('email'), children: t('Auth.EMAIL_LABEL', 'Email') }) }), _jsx(TableCell, { sx: cellSx, sortDirection: sortConfig.key === 'name' ? sortConfig.direction : false, children: _jsx(TableSortLabel, { active: sortConfig.key === 'name', direction: sortConfig.key === 'name' ? sortConfig.direction : 'asc', onClick: () => handleSort('name'), children: t('Profile.NAME_LABEL', 'Name') }) }), _jsx(TableCell, { sx: cellSx, sortDirection: sortConfig.key === 'role' ? sortConfig.direction : false, children: _jsx(TableSortLabel, { active: sortConfig.key === 'role', direction: sortConfig.key === 'role' ? sortConfig.direction : 'asc', onClick: () => handleSort('role'), children: t('UserList.ROLE', 'Role') }) }), _jsx(TableCell, { sx: cellSx, sortDirection: sortConfig.key === 'is_support_agent' ? sortConfig.direction : false, children: _jsx(TableSortLabel, { active: sortConfig.key === 'is_support_agent', direction: sortConfig.key === 'is_support_agent' ? sortConfig.direction : 'asc', onClick: () => handleSort('is_support_agent'), children: t('UserList.SUPPORTER', 'Support Agent') }) }), visibleExtraColumns.map((column) => (_jsx(TableCell, { sx: cellSx, align: column.align || 'left', sortDirection: sortConfig.key === `extra:${column.key}` ? sortConfig.direction : false, children: _jsx(TableSortLabel, { active: sortConfig.key === `extra:${column.key}`, direction: sortConfig.key === `extra:${column.key}` ? sortConfig.direction : 'asc', onClick: () => handleSort(`extra:${column.key}`), children: typeof column.label === 'function' ? column.label(listContext) : column.label }) }, column.key))), _jsx(TableCell, { sx: cellSx, children: t('Common.ACTIONS', 'Actions') })] }) }), _jsxs(TableBody, { children: [filteredAndSortedUsers.map((u) => (_jsxs(TableRow, { children: [_jsx(TableCell, { sx: cellSx, children: u.email }), _jsx(TableCell, { sx: cellSx, children: getUserDisplayName(u) }), _jsx(TableCell, { sx: cellSx, children: _jsx(FormControl, { size: "small", fullWidth: true, sx: controlSx, disabled: !canEdit(u), children: _jsx(Select, { value: u.role || 'none', onChange: (e) => handleChangeRole(u.id, e.target.value), variant: "outlined", children: roles.map(r => _jsx(MenuItem, { value: r, children: r }, r)) }) }) }), _jsx(TableCell, { sx: cellSx, children: _jsx(FormControl, { size: "small", fullWidth: true, sx: controlSx, disabled: !canEdit(u), children: _jsxs(Select, { value: u.is_support_agent ? 'yes' : 'no', onChange: (e) => handleToggleSupporter(u.id, e.target.value === 'yes'), variant: "outlined", children: [_jsx(MenuItem, { value: "yes", children: t('Common.YES', 'Yes') }), _jsx(MenuItem, { value: "no", children: t('Common.NO', 'No') })] }) }) }), visibleExtraColumns.map((column) => (_jsx(TableCell, { sx: cellSx, align: column.align || 'left', children: column.renderCell({
|
|
120
262
|
user: u,
|
|
121
263
|
canEdit: canEdit(u),
|
|
122
264
|
currentUser,
|
|
123
265
|
extraContext,
|
|
124
266
|
t,
|
|
125
267
|
reloadUsers: loadUsers,
|
|
126
|
-
}) }, `${column.key}-${u.id}`))),
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
268
|
+
}) }, `${column.key}-${u.id}`))), _jsx(TableCell, { sx: cellSx, children: _jsxs(Box, { sx: { display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }, children: [visibleRowActions.map((action) => {
|
|
269
|
+
const actionId = `${action.key}:${u.id}`;
|
|
270
|
+
const isBusy = Boolean(rowActionLoading[actionId]);
|
|
271
|
+
const isDisabled = typeof action.disabled === 'function'
|
|
272
|
+
? action.disabled({
|
|
273
|
+
user: u,
|
|
274
|
+
canEdit: canEdit(u),
|
|
275
|
+
currentUser,
|
|
276
|
+
extraContext,
|
|
277
|
+
t,
|
|
278
|
+
})
|
|
279
|
+
: false;
|
|
280
|
+
return (_jsx(Button, { size: "small", variant: "outlined", onClick: () => runRowAction(action, u), disabled: isBusy || isDisabled, sx: actionButtonSx, children: typeof action.label === 'function'
|
|
281
|
+
? action.label({ user: u, t, currentUser, canEdit: canEdit(u) })
|
|
282
|
+
: action.label }, `${action.key}-${u.id}`));
|
|
283
|
+
}), _jsx(Tooltip, { title: t('Common.DELETE'), children: _jsx("span", { children: _jsx(Button, { size: "small", variant: "outlined", color: "error", startIcon: _jsx(DeleteIcon, {}), onClick: () => handleDelete(u.id), disabled: !canEdit(u), sx: actionButtonSx, children: t('Common.DELETE', 'Delete') }) }) })] }) })] }, u.id))), filteredAndSortedUsers.length === 0 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5 + visibleExtraColumns.length, align: "center", children: t('UserList.NO_USERS', 'No users found.') }) }))] })] }) }))] }));
|
|
142
284
|
}
|
|
@@ -810,6 +810,36 @@ export const authTranslations = {
|
|
|
810
810
|
"en": "I allow convenience cookies.",
|
|
811
811
|
"sw": "Ninaruhusu vidakuzi vya urahisi."
|
|
812
812
|
},
|
|
813
|
+
"Profile.PRIVACY_STATEMENT_TITLE": {
|
|
814
|
+
"de": "Datenschutzerklärung",
|
|
815
|
+
"fr": "Déclaration de confidentialité",
|
|
816
|
+
"en": "Privacy statement",
|
|
817
|
+
"sw": "Taarifa ya faragha"
|
|
818
|
+
},
|
|
819
|
+
"Profile.COOKIES_STATEMENT_TITLE": {
|
|
820
|
+
"de": "Cookie-Richtlinie",
|
|
821
|
+
"fr": "Politique relative aux cookies",
|
|
822
|
+
"en": "Cookie statement",
|
|
823
|
+
"sw": "Taarifa ya vidakuzi"
|
|
824
|
+
},
|
|
825
|
+
"Profile.STATEMENT_LOADING": {
|
|
826
|
+
"de": "Statement wird geladen...",
|
|
827
|
+
"fr": "Chargement du document...",
|
|
828
|
+
"en": "Loading statement...",
|
|
829
|
+
"sw": "Inapakia taarifa..."
|
|
830
|
+
},
|
|
831
|
+
"Profile.PRIVACY_STATEMENT_EMPTY": {
|
|
832
|
+
"de": "Die Datenschutzerklärung ist derzeit nicht verfügbar.",
|
|
833
|
+
"fr": "La déclaration de confidentialité n'est actuellement pas disponible.",
|
|
834
|
+
"en": "Privacy statement is currently unavailable.",
|
|
835
|
+
"sw": "Taarifa ya faragha haipatikani kwa sasa."
|
|
836
|
+
},
|
|
837
|
+
"Profile.COOKIES_STATEMENT_EMPTY": {
|
|
838
|
+
"de": "Die Cookie-Richtlinie ist derzeit nicht verfügbar.",
|
|
839
|
+
"fr": "La politique relative aux cookies n'est actuellement pas disponible.",
|
|
840
|
+
"en": "Cookie statement is currently unavailable.",
|
|
841
|
+
"sw": "Taarifa ya vidakuzi haipatikani kwa sasa."
|
|
842
|
+
},
|
|
813
843
|
"Profile.SAVE_BUTTON": {
|
|
814
844
|
"de": "Speichern",
|
|
815
845
|
"fr": "Enregistrer",
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import React, { useContext, useMemo } from 'react';
|
|
3
3
|
import { Helmet } from 'react-helmet';
|
|
4
4
|
import { useSearchParams } from 'react-router-dom';
|
|
5
|
-
import { Tabs, Tab, Box, Typography, Alert, CircularProgress } from '@mui/material';
|
|
5
|
+
import { Tabs, Tab, Box, Typography, Alert, CircularProgress, Paper, Stack, } from '@mui/material';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
// Internal Library Components
|
|
8
8
|
// Stellen Sie sicher, dass diese Pfade korrekt auf Ihre Library zeigen!
|
|
@@ -27,7 +27,10 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
27
27
|
const { user, login, loading } = useContext(AuthContext);
|
|
28
28
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
29
29
|
// 1. URL State Management
|
|
30
|
-
const
|
|
30
|
+
const currentTabRaw = searchParams.get('tab') || 'profile';
|
|
31
|
+
const currentTab = ['invite', 'bulk-invite-csv', 'access'].includes(currentTabRaw)
|
|
32
|
+
? 'invite'
|
|
33
|
+
: currentTabRaw;
|
|
31
34
|
const fromRecovery = searchParams.get('from') === 'recovery';
|
|
32
35
|
const fromWeakLogin = searchParams.get('from') === 'weak_login';
|
|
33
36
|
// 2. Data & Permissions
|
|
@@ -55,17 +58,8 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
55
58
|
if (isSuperUser || perms.can_view_users) {
|
|
56
59
|
list.push({ value: 'users', label: t('Account.TAB_USERS', 'Users') });
|
|
57
60
|
}
|
|
58
|
-
if (isSuperUser || perms.can_invite) {
|
|
61
|
+
if (isSuperUser || perms.can_invite || perms.can_manage_access_codes) {
|
|
59
62
|
list.push({ value: 'invite', label: t('Account.TAB_INVITE', 'Invite') });
|
|
60
|
-
if (showBulkInviteCsvTab) {
|
|
61
|
-
list.push({
|
|
62
|
-
value: 'bulk-invite-csv',
|
|
63
|
-
label: t('Account.TAB_BULK_INVITE_CSV', 'Bulk Invite CSV'),
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (isSuperUser || perms.can_manage_access_codes) {
|
|
68
|
-
list.push({ value: 'access', label: t('Account.TAB_ACCESS_CODES', 'Access Codes') });
|
|
69
63
|
}
|
|
70
64
|
if (isSuperUser || perms.can_view_support) {
|
|
71
65
|
list.push({ value: 'support', label: t('Account.TAB_SUPPORT', 'Support') });
|
|
@@ -82,7 +76,7 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
82
76
|
}
|
|
83
77
|
});
|
|
84
78
|
return list;
|
|
85
|
-
}, [user, perms, t, isSuperUser,
|
|
79
|
+
}, [user, perms, t, isSuperUser, extraTabs]);
|
|
86
80
|
// 4. Loading & Auth Checks
|
|
87
81
|
if (loading) {
|
|
88
82
|
return (_jsx(Box, { sx: { display: 'flex', justifyContent: 'center', mt: 10 }, children: _jsx(CircularProgress, {}) }));
|
|
@@ -94,9 +88,9 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
94
88
|
const activeTabExists = tabs.some(t => t.value === currentTab);
|
|
95
89
|
// Falls der Tab nicht erlaubt ist (z.B. manuell in URL eingegeben), Fallback auf 'profile'
|
|
96
90
|
const safeTab = activeTabExists ? currentTab : 'profile';
|
|
97
|
-
const builtInTabValues = new Set(['profile', 'security', 'users', 'invite', '
|
|
91
|
+
const builtInTabValues = new Set(['profile', 'security', 'users', 'invite', 'support']);
|
|
98
92
|
const activeExtraTab = builtInTabValues.has(safeTab)
|
|
99
93
|
? null
|
|
100
94
|
: extraTabs.find((tab) => tab.value === safeTab);
|
|
101
|
-
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserInviteComponent, {}) })),
|
|
95
|
+
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsx(Tabs, { value: safeTab, onChange: handleTabChange, variant: "scrollable", scrollButtons: "auto", sx: { mb: 3, borderBottom: 1, borderColor: 'divider' }, children: tabs.map((tab) => (_jsx(Tab, { label: tab.label, value: tab.value }, tab.value))) }), safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [(isSuperUser || perms.can_invite) && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(UserInviteComponent, {}) })), (isSuperUser || perms.can_invite) && showBulkInviteCsvTab && (_jsx(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), (isSuperUser || perms.can_manage_access_codes) && (_jsxs(Paper, { variant: "outlined", sx: { p: 2.5, borderRadius: 2 }, children: [_jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] }));
|
|
102
96
|
}
|