@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.
@@ -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
- // Map data to ensure consistent structure
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 {
@@ -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, { sx: { mt: 2 }, 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
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, { sx: { mt: 1 }, 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) => {
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
- export function ProfileComponent({ onSubmit, submitText, showName = true, showPrivacy = true, showCookies = true, }) {
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, mt: 2 }, 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) => {
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, IconButton, Tooltip, CircularProgress, Alert } from '@mui/material';
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
- setUsers(data);
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: [users.map((u) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: u.email }), _jsx(TableCell, { children: u.first_name || u.last_name ? `${u.first_name} ${u.last_name}` : u.username }), _jsx(TableCell, { children: _jsx(FormControl, { size: "small", fullWidth: true, disabled: !canEdit(u), children: _jsx(Select, { value: u.role || 'none', onChange: (e) => handleChangeRole(u.id, e.target.value), variant: "standard", disableUnderline: true, children: roles.map(r => _jsx(MenuItem, { value: r, children: r }, r)) }) }) }), _jsx(TableCell, { children: _jsx(Button, { size: "small", variant: u.is_support_agent ? 'contained' : 'outlined', color: u.is_support_agent ? 'primary' : 'inherit', onClick: () => handleToggleSupporter(u.id, !u.is_support_agent), disabled: !canEdit(u), sx: { textTransform: 'none' }, children: u.is_support_agent ? t('Common.YES') : t('Common.NO') }) }), visibleExtraColumns.map((column) => (_jsx(TableCell, { align: column.align || 'left', children: column.renderCell({
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}`))), _jsxs(TableCell, { children: [visibleRowActions.map((action) => {
127
- const actionId = `${action.key}:${u.id}`;
128
- const isBusy = Boolean(rowActionLoading[actionId]);
129
- const isDisabled = typeof action.disabled === 'function'
130
- ? action.disabled({
131
- user: u,
132
- canEdit: canEdit(u),
133
- currentUser,
134
- extraContext,
135
- t,
136
- })
137
- : false;
138
- return (_jsx(Button, { size: "small", onClick: () => runRowAction(action, u), disabled: isBusy || isDisabled, sx: { mr: 1, mb: 0.5, textTransform: 'none' }, children: typeof action.label === 'function'
139
- ? action.label({ user: u, t, currentUser, canEdit: canEdit(u) })
140
- : action.label }, `${action.key}-${u.id}`));
141
- }), _jsx(Tooltip, { title: t('Common.DELETE'), children: _jsx("span", { children: _jsx(IconButton, { onClick: () => handleDelete(u.id), color: "error", disabled: !canEdit(u), children: _jsx(DeleteIcon, {}) }) }) })] })] }, u.id))), users.length === 0 && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 5 + visibleExtraColumns.length, align: "center", children: t('UserList.NO_USERS', 'No users found.') }) }))] })] }) }))] }));
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 currentTab = searchParams.get('tab') || 'profile';
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, showBulkInviteCsvTab, extraTabs]);
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', 'bulk-invite-csv', 'access', 'support']);
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, {}) })), safeTab === 'bulk-invite-csv' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), safeTab === 'access' && (_jsxs(Box, { sx: { mt: 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 }) }))] }));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.1.16",
3
+ "version": "2.1.17",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "private": false,