implemented feedback

This commit is contained in:
Ida Dittrich 2025-09-16 12:20:02 +02:00
parent 9fc33c7c73
commit 9e7c3b2aa7
36 changed files with 3055 additions and 508 deletions

View file

@ -32,6 +32,17 @@
background-color: var(--color-primary-hover); background-color: var(--color-primary-hover);
} }
/* Expired connection highlighting */
.connectionsTable :global(.expired-connection) {
background-color: rgba(255, 193, 7, 0.1) !important;
color: #ff6b35 !important;
font-weight: 600;
}
.connectionsTable :global(.tr:hover .expired-connection) {
background-color: rgba(255, 193, 7, 0.2) !important;
}
/* Responsive design */ /* Responsive design */
@media (max-width: 768px) { @media (max-width: 768px) {
.tableContainer { .tableContainer {

View file

@ -38,7 +38,7 @@ export interface ConnectionsErrorDisplayProps {
// Table Action Interface // Table Action Interface
export interface TableAction { export interface TableAction {
label: string; label: string;
onClick: (connection: Connection) => Promise<void> | void; onClick?: (connection: Connection) => Promise<void> | void;
icon: React.ReactNode | ((connection: Connection) => React.ReactNode); icon: React.ReactNode | ((connection: Connection) => React.ReactNode);
} }
@ -53,7 +53,7 @@ export interface ConnectionHandlers {
handleDisconnect: (connection: Connection) => Promise<void>; handleDisconnect: (connection: Connection) => Promise<void>;
handleDelete: (connection: Connection) => Promise<void>; handleDelete: (connection: Connection) => Promise<void>;
handleDeleteMultiple: (connections: Connection[]) => Promise<void>; handleDeleteMultiple: (connections: Connection[]) => Promise<void>;
handleConnectOrDisconnect: (connection: Connection) => Promise<void>; handleUpdateConnection: (connection: Connection) => Promise<void>;
handleEditConnection: (connection: Connection) => Promise<void>; handleEditConnection: (connection: Connection) => Promise<void>;
handleSaveConnection: (updatedConnection: Connection) => Promise<void>; handleSaveConnection: (updatedConnection: Connection) => Promise<void>;
handleCancelEdit: () => void; handleCancelEdit: () => void;

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { IoIosLink, IoIosTrash } from 'react-icons/io'; import { IoIosRefresh, IoIosTrash } from 'react-icons/io';
import { MdModeEdit } from 'react-icons/md'; import { MdModeEdit } from 'react-icons/md';
import { GoUnlink } from 'react-icons/go';
import { useConnections, useOAuthConnect, useDisconnect } from '../../hooks/useConnections'; import { useConnections, useOAuthConnect, useDisconnect } from '../../hooks/useConnections';
@ -25,6 +25,8 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
createConnection, createConnection,
updateConnection, updateConnection,
deleteConnection, deleteConnection,
refreshMicrosoftToken,
refreshGoogleToken,
isLoading, isLoading,
error error
} = useConnections(); } = useConnections();
@ -167,11 +169,15 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
type: 'enum', type: 'enum',
filterOptions: ['active', 'pending', 'expired', 'revoked'], filterOptions: ['active', 'pending', 'expired', 'revoked'],
formatter: (value: string) => { formatter: (value: string) => {
return value?.charAt(0).toUpperCase() + value?.slice(1) || t('connections.unknown', 'Unknown'); const status = value?.charAt(0).toUpperCase() + value?.slice(1) || t('connections.unknown', 'Unknown');
return status;
}, },
width: 120, width: 120,
sortable: true, sortable: true,
filterable: true filterable: true,
cellClassName: (value: string) => {
return value === 'expired' ? 'expired-connection' : '';
}
}, },
{ {
key: 'externalUsername', key: 'externalUsername',
@ -282,9 +288,29 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
} }
]; ];
// Fetch connections on mount // Fetch connections on mount and auto-connect them
useEffect(() => { useEffect(() => {
fetchConnections(); const initializeConnections = async () => {
try {
await fetchConnections();
// Auto-connect all connections that are not active
const connectionsToConnect = connections.filter(conn =>
conn.status !== 'active' && conn.authority !== 'local'
);
for (const connection of connectionsToConnect) {
try {
await connectWithPopup(connection.id);
} catch (error) {
console.warn(`Failed to auto-connect ${connection.authority} connection:`, error);
}
}
} catch (error) {
console.error('Error initializing connections:', error);
}
};
initializeConnections();
}, []); }, []);
// Handler functions // Handler functions
@ -362,11 +388,23 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
} }
}; };
const handleConnectOrDisconnect = async (connection: Connection) => { const handleUpdateConnection = async (connection: Connection) => {
if (connection.status === 'active') { console.log('Updating connection:', connection);
await handleDisconnect(connection); try {
} else { if (connection.status === 'expired') {
await handleConnect(connection); // Refresh token based on connection type
if (connection.authority === 'msft') {
await refreshMicrosoftToken(connection.id);
} else if (connection.authority === 'google') {
await refreshGoogleToken(connection.id);
}
} else if (connection.status !== 'active') {
// If not active and not expired, try to connect
await handleConnect(connection);
}
await fetchConnections();
} catch (error) {
console.error('Error updating connection:', error);
} }
}; };
@ -408,14 +446,14 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
icon: <MdModeEdit /> icon: <MdModeEdit />
}, },
{ {
label: t('connections.action.toggle_connection', 'Toggle Connection'), label: t('connections.action.update', 'Update'),
onClick: handleConnectOrDisconnect, onClick: handleUpdateConnection,
icon: (connection: Connection) => connection.status === 'active' ? <GoUnlink /> : <IoIosLink /> icon: <IoIosRefresh />
}, },
{ {
label: t('connections.action.delete', 'Delete'), label: t('connections.action.delete', 'Delete'),
onClick: handleDelete, icon: <IoIosTrash />,
icon: <IoIosTrash /> // onClick is handled by FormGenerator for delete confirmation
} }
]; ];
@ -440,7 +478,7 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
handleDisconnect, handleDisconnect,
handleDelete, handleDelete,
handleDeleteMultiple, handleDeleteMultiple,
handleConnectOrDisconnect, handleUpdateConnection,
handleEditConnection, handleEditConnection,
handleSaveConnection, handleSaveConnection,
handleCancelEdit handleCancelEdit

View file

@ -16,7 +16,7 @@ export interface DateienTableProps {
// Table Action Interface // Table Action Interface
export interface TableAction { export interface TableAction {
label: string; label: string;
onClick: (file: UserFile) => Promise<void> | void; onClick?: (file: UserFile) => Promise<void> | void;
icon: React.ReactNode | ((file: UserFile) => React.ReactNode); icon: React.ReactNode | ((file: UserFile) => React.ReactNode);
} }

View file

@ -308,48 +308,43 @@ export function useDateienLogic(): DateienLogicReturn {
// Handle file deletion // Handle file deletion
const handleDelete = async (file: UserFile) => { const handleDelete = async (file: UserFile) => {
if (window.confirm(t('files.delete.confirm').replace('{name}', file.file_name))) { // Immediately remove from UI for instant feedback
// Immediately remove from UI for instant feedback removeFileOptimistically(file.id);
removeFileOptimistically(file.id);
const success = await handleFileDelete(file.id); const success = await handleFileDelete(file.id);
if (!success && deleteError) { if (!success && deleteError) {
console.error('Delete failed:', deleteError); console.error('Delete failed:', deleteError);
// Refetch to restore the file in case of failure // Refetch to restore the file in case of failure
refetch(); refetch();
}
} }
}; };
// Handle multiple file deletion // Handle multiple file deletion
const handleDeleteMultiple = async (filesToDelete: UserFile[]) => { const handleDeleteMultiple = async (filesToDelete: UserFile[]) => {
const fileCount = filesToDelete.length; // Immediately remove all files from UI for instant feedback
if (window.confirm(t('files.delete.confirmMultiple', 'Are you sure you want to delete {count} files?').replace('{count}', fileCount.toString()))) { filesToDelete.forEach(file => removeFileOptimistically(file.id));
// Immediately remove all files from UI for instant feedback
filesToDelete.forEach(file => removeFileOptimistically(file.id));
// Start all delete operations simultaneously // Start all delete operations simultaneously
const deletePromises = filesToDelete.map(async (file) => { const deletePromises = filesToDelete.map(async (file) => {
try { try {
const success = await handleFileDelete(file.id); const success = await handleFileDelete(file.id);
return { fileId: file.id, success }; return { fileId: file.id, success };
} catch (error) { } catch (error) {
console.error('Failed to delete file:', file.id, error); console.error('Failed to delete file:', file.id, error);
return { fileId: file.id, success: false }; return { fileId: file.id, success: false };
}
});
// Wait for all deletions to complete
const results = await Promise.all(deletePromises);
// Check if any deletions failed
const failedDeletions = results.filter(result => !result.success);
if (failedDeletions.length > 0) {
console.error('Some file deletions failed:', failedDeletions);
// Refetch to restore any files that failed to delete
refetch();
} }
});
// Wait for all deletions to complete
const results = await Promise.all(deletePromises);
// Check if any deletions failed
const failedDeletions = results.filter(result => !result.success);
if (failedDeletions.length > 0) {
console.error('Some file deletions failed:', failedDeletions);
// Refetch to restore any files that failed to delete
refetch();
} }
}; };
@ -390,18 +385,10 @@ export function useDateienLogic(): DateienLogicReturn {
}, },
{ {
label: t('files.action.delete'), label: t('files.action.delete'),
icon: (row: UserFile) => { icon: <IoIosTrash />,
const isDeletingThis = deletingFiles.has(row.id); // onClick is handled by FormGenerator for delete confirmation
if (isDeletingThis) return '⏳';
return <IoIosTrash />;
},
onClick: (row: UserFile) => {
if (!deletingFiles.has(row.id)) {
handleDelete(row);
}
}
} }
], [t, previewingFiles, downloadingFiles, deletingFiles, handleDownload, handleDelete]); ], [t, previewingFiles, downloadingFiles, handleDownload, handleDelete]);
return { return {
files, files,

View file

@ -334,6 +334,26 @@
table-layout: fixed; table-layout: fixed;
} }
/* Disabled user row styling */
.table tbody tr[data-user-enabled="false"] {
opacity: 0.6 !important;
background-color: rgba(0, 0, 0, 0.02) !important;
}
.table tbody tr[data-user-enabled="false"]:hover {
opacity: 0.8 !important;
background-color: rgba(0, 0, 0, 0.05) !important;
}
/* Dark mode disabled user styling */
.dark .table tbody tr[data-user-enabled="false"] {
background-color: rgba(255, 255, 255, 0.02) !important;
}
.dark .table tbody tr[data-user-enabled="false"]:hover {
background-color: rgba(255, 255, 255, 0.05) !important;
}
.th { .th {
position: sticky; position: sticky;
top: 0; top: 0;
@ -401,7 +421,7 @@
} }
.tr:hover { .tr:hover {
background: var(--color-gray-disabled); background: transparent;
} }
.tr.selected { .tr.selected {
@ -514,7 +534,42 @@ tbody .actionsColumn {
justify-content: center; justify-content: center;
} }
/* Pagination */ /* Delete Confirmation Buttons */
.deleteConfirmButtons {
display: flex;
gap: 2px;
justify-content: center;
align-items: center;
background: var(--color-secondary);
border-radius: 25px;
}
.confirmButton {
background: transparent !important;
color: white !important;
}
.confirmButton:hover {
background: transparent !important;
transform: scale(1.05);
}
.cancelButton {
background: transparent !important;
color: white !important;
}
.cancelButton:hover {
background: transparent !important;
transform: scale(1.05);
}
.actionButton:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
}
.pagination { .pagination {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -684,7 +739,7 @@ tbody .actionsColumn {
} }
.tr:hover { .tr:hover {
background: rgba(255, 255, 255, 0.05); background: transparent;
} }
.tr.selected { .tr.selected {

View file

@ -2,7 +2,7 @@ import React, { useState, useMemo, useRef, useEffect } from 'react';
import { useLanguage } from '../../contexts/LanguageContext'; import { useLanguage } from '../../contexts/LanguageContext';
import styles from './FormGenerator.module.css'; import styles from './FormGenerator.module.css';
import { IoIosRefresh } from "react-icons/io"; import { IoIosRefresh, IoIosCheckmark, IoIosClose } from "react-icons/io";
// Types for the FormGenerator // Types for the FormGenerator
export interface ColumnConfig { export interface ColumnConfig {
@ -17,6 +17,7 @@ export interface ColumnConfig {
searchable?: boolean; searchable?: boolean;
formatter?: (value: any, row: any) => React.ReactNode; formatter?: (value: any, row: any) => React.ReactNode;
filterOptions?: string[]; // For enum/select filters filterOptions?: string[]; // For enum/select filters
cellClassName?: (value: any, row: any) => string; // For custom cell styling
} }
export interface FormGeneratorProps<T = any> { export interface FormGeneratorProps<T = any> {
@ -45,6 +46,7 @@ export interface FormGeneratorProps<T = any> {
onDeleteMultiple?: (rows: T[]) => void; onDeleteMultiple?: (rows: T[]) => void;
onRefresh?: () => void; onRefresh?: () => void;
className?: string; className?: string;
getRowDataAttributes?: (row: T, index: number) => Record<string, string>;
} }
export function FormGenerator<T extends Record<string, any>>({ export function FormGenerator<T extends Record<string, any>>({
@ -67,7 +69,8 @@ export function FormGenerator<T extends Record<string, any>>({
onDelete, onDelete,
onDeleteMultiple, onDeleteMultiple,
onRefresh, onRefresh,
className = '' className = '',
getRowDataAttributes
}: FormGeneratorProps<T>) { }: FormGeneratorProps<T>) {
const { t } = useLanguage(); const { t } = useLanguage();
// Auto-detect columns if not provided // Auto-detect columns if not provided
@ -115,6 +118,13 @@ export function FormGenerator<T extends Record<string, any>>({
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [currentPageSize, setCurrentPageSize] = useState(pageSize); const [currentPageSize, setCurrentPageSize] = useState(pageSize);
// Delete confirmation state
const [deleteConfirmRow, setDeleteConfirmRow] = useState<number | null>(null);
const [deletingRows, setDeletingRows] = useState<Set<number>>(new Set());
// Refs for action buttons containers to detect clicks outside
const actionButtonsRefs = useRef<Map<number, HTMLDivElement>>(new Map());
// Refs for resizing // Refs for resizing
const tableRef = useRef<HTMLTableElement>(null); const tableRef = useRef<HTMLTableElement>(null);
const resizingColumn = useRef<string | null>(null); const resizingColumn = useRef<string | null>(null);
@ -132,6 +142,28 @@ export function FormGenerator<T extends Record<string, any>>({
setColumnWidths(initialWidths); setColumnWidths(initialWidths);
}, [detectedColumns]); }, [detectedColumns]);
// Handle clicks outside delete confirmation buttons
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (deleteConfirmRow !== null) {
const actionButtonsRef = actionButtonsRefs.current.get(deleteConfirmRow);
if (actionButtonsRef) {
// Check if the click is outside the action buttons container for this specific row
if (!actionButtonsRef.contains(event.target as Node)) {
setDeleteConfirmRow(null);
}
}
}
};
if (deleteConfirmRow !== null) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
}, [deleteConfirmRow]);
// Filter and search data // Filter and search data
const filteredData = useMemo(() => { const filteredData = useMemo(() => {
let result = [...data]; let result = [...data];
@ -293,6 +325,43 @@ export function FormGenerator<T extends Record<string, any>>({
} }
}; };
// Handle delete confirmation
const handleDeleteConfirm = (_row: T, index: number) => {
setDeleteConfirmRow(index);
};
// Handle delete confirmation - confirm
const handleDeleteConfirmYes = async (row: T, index: number) => {
if (onDelete) {
setDeletingRows(prev => new Set(prev).add(index));
try {
await onDelete(row);
// Remove from selection if it was selected
if (selectedRows.has(index)) {
const newSelected = new Set(selectedRows);
newSelected.delete(index);
setSelectedRows(newSelected);
if (onRowSelect) {
const selectedData = Array.from(newSelected).map(i => paginatedData[i]);
onRowSelect(selectedData);
}
}
} finally {
setDeletingRows(prev => {
const newSet = new Set(prev);
newSet.delete(index);
return newSet;
});
setDeleteConfirmRow(null);
}
}
};
// Handle delete confirmation - cancel
const handleDeleteConfirmNo = () => {
setDeleteConfirmRow(null);
};
// Handle delete multiple items // Handle delete multiple items
const handleDeleteMultiple = () => { const handleDeleteMultiple = () => {
if (onDeleteMultiple && selectedRows.size > 0) { if (onDeleteMultiple && selectedRows.size > 0) {
@ -680,12 +749,17 @@ export function FormGenerator<T extends Record<string, any>>({
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{paginatedData.map((row, index) => ( {paginatedData.map((row, index) => {
<tr const dataAttributes = getRowDataAttributes ? getRowDataAttributes(row, index) : {};
key={index} return (
className={`${styles.tr} ${selectedRows.has(index) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`} <tr
onClick={() => onRowClick?.(row, index)} key={index}
> className={`${styles.tr} ${selectedRows.has(index) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`}
onClick={() => onRowClick?.(row, index)}
{...Object.fromEntries(
Object.entries(dataAttributes).map(([key, value]) => [`data-${key}`, value])
)}
>
{selectable && ( {selectable && (
<td className={styles.selectColumn} style={{ width: '50px', minWidth: '50px', maxWidth: '50px' }}> <td className={styles.selectColumn} style={{ width: '50px', minWidth: '50px', maxWidth: '50px' }}>
<input <input
@ -711,18 +785,80 @@ export function FormGenerator<T extends Record<string, any>>({
className={styles.actionsColumn} className={styles.actionsColumn}
style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }} style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}
> >
<div className={styles.actionButtons}> <div
ref={(el) => {
if (el) {
actionButtonsRefs.current.set(index, el);
} else {
actionButtonsRefs.current.delete(index);
}
}}
className={styles.actionButtons}
>
{actions.map((action, actionIndex) => { {actions.map((action, actionIndex) => {
const actionLabel = typeof action.label === 'function' ? action.label(row) : action.label; const actionLabel = typeof action.label === 'function' ? action.label(row) : action.label;
const isDeleteAction = actionLabel.toLowerCase().includes('delete') ||
actionLabel.toLowerCase().includes('löschen') ||
actionLabel.toLowerCase().includes('supprimer') ||
(typeof action.label === 'string' && action.label.toLowerCase().includes('delete'));
const isConfirmingDelete = deleteConfirmRow === index && isDeleteAction;
const isDeleting = deletingRows.has(index);
// Check if delete action is disabled (e.g., for system prompts)
const isDeleteDisabled = isDeleteAction && (
actionLabel.toLowerCase().includes('disabled') ||
actionLabel.toLowerCase().includes('no permission') ||
actionLabel.toLowerCase().includes('keine berechtigung')
);
if (isConfirmingDelete) {
return (
<div key={actionIndex} className={styles.deleteConfirmButtons}>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteConfirmYes(row, index);
}}
className={`${styles.actionButton} ${styles.confirmButton}`}
title={t('formgen.delete.confirm', 'Confirm delete')}
disabled={isDeleting}
>
<span className={styles.actionIcon}>
<IoIosCheckmark />
</span>
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteConfirmNo();
}}
className={`${styles.actionButton} ${styles.cancelButton}`}
title={t('formgen.delete.cancel', 'Cancel delete')}
disabled={isDeleting}
>
<span className={styles.actionIcon}>
<IoIosClose />
</span>
</button>
</div>
);
}
return ( return (
<button <button
key={actionIndex} key={actionIndex}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
action.onClick(row); if (isDeleteAction && !isDeleteDisabled) {
handleDeleteConfirm(row, index);
} else if (action.onClick) {
action.onClick(row);
}
}} }}
className={styles.actionButton} className={styles.actionButton}
title={actionLabel} title={actionLabel}
disabled={isDeleting || (deleteConfirmRow !== null && deleteConfirmRow !== index) || isDeleteDisabled}
> >
{action.icon && ( {action.icon && (
<span className={styles.actionIcon}> <span className={styles.actionIcon}>
@ -735,22 +871,29 @@ export function FormGenerator<T extends Record<string, any>>({
</div> </div>
</td> </td>
)} )}
{detectedColumns.map(column => ( {detectedColumns.map(column => {
<td const cellValue = row[column.key];
key={column.key} const customClassName = column.cellClassName ? column.cellClassName(cellValue, row) : '';
className={styles.td} const combinedClassName = `${styles.td} ${customClassName}`.trim();
style={{
width: columnWidths[column.key] || column.width || 150, return (
minWidth: columnWidths[column.key] || column.width || 150, <td
maxWidth: columnWidths[column.key] || column.width || 150 key={column.key}
}} className={combinedClassName}
> style={{
{formatCellValue(row[column.key], column, row)} width: columnWidths[column.key] || column.width || 150,
</td> minWidth: columnWidths[column.key] || column.width || 150,
))} maxWidth: columnWidths[column.key] || column.width || 150
}}
>
{formatCellValue(cellValue, column, row)}
</td>
);
})}
</tr> </tr>
))} );
})}
</tbody> </tbody>
</table> </table>
)} )}

View file

@ -7,6 +7,47 @@
gap: 20px; gap: 20px;
} }
/* Header container with title and add button */
.headerContainer {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.tableTitle {
font-size: 1.5rem;
font-weight: 400;
color: var(--color-text);
margin: 0;
font-family: var(--font-family);
}
.addUserButton {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(--color-secondary);
color: white;
border: none;
border-radius: 25px;
font-size: 14px;
font-family: var(--font-family);
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.addUserButton:hover {
background: var(--color-secondary-hover);
transform: translateY(-1px);
}
.addUserButton:active {
transform: translateY(0);
}
/* FormGenerator container */ /* FormGenerator container */
.mitgliederFormGenerator { .mitgliederFormGenerator {
flex: 1; flex: 1;
@ -62,3 +103,61 @@
font-size: 0.85em; font-size: 0.85em;
} }
.userPrivilege {
color: var(--color-text);
font-size: 0.85em;
}
.userEnabled {
font-size: 0.85em;
font-weight: 500;
}
.userEnabled.enabled {
color: var(--color-success, #28a745);
}
.userEnabled.disabled {
color: var(--color-danger, #dc3545);
}
.userAuthAuthority {
color: var(--color-gray);
font-size: 0.85em;
}
/* Delete confirmation dialog styles */
.deleteConfirmation {
padding: 20px;
text-align: center;
color: var(--color-text);
font-family: var(--font-family);
}
.deleteConfirmation p {
margin: 0 0 20px 0;
font-size: 16px;
line-height: 1.5;
}
.userInfo {
margin: 10px 0;
padding: 10px;
background: var(--color-gray-disabled);
border-radius: 8px;
text-align: left;
}
.userInfo strong {
color: var(--color-secondary);
margin-right: 8px;
}
.warning {
margin-top: 20px !important;
color: var(--color-danger, #dc3545);
font-weight: 500;
font-size: 14px;
}

View file

@ -1,10 +1,12 @@
import { FormGenerator } from '../FormGenerator/FormGenerator'; import { FormGenerator } from '../FormGenerator/FormGenerator';
import { Popup } from '../Popup/Popup';
import { EditForm, EditFieldConfig } from '../Popup/EditForm';
import { useMitgliederLogic } from './mitgliederLogic'; import { useMitgliederLogic } from './mitgliederLogic';
import { MitgliederTableProps } from './mitgliederTypes'; import { MitgliederTableProps } from './mitgliederTypes';
import { useLanguage } from '../../contexts/LanguageContext'; import { useLanguage } from '../../contexts/LanguageContext';
import styles from './MitgliederTable.module.css'; import styles from './MitgliederTable.module.css';
function MitgliederTable({ className = '' }: MitgliederTableProps) { function MitgliederTable({ className = '', showAddUser = false, onAddUserClose }: MitgliederTableProps) {
const { t } = useLanguage(); const { t } = useLanguage();
const { const {
users, users,
@ -12,9 +14,132 @@ function MitgliederTable({ className = '' }: MitgliederTableProps) {
error, error,
columns, columns,
actions, actions,
refetch refetch,
editingUser,
handleSaveUser,
handleCancelEdit,
deletingUser,
handleConfirmDelete,
handleCancelDelete,
handleSaveNewUser
} = useMitgliederLogic(); } = useMitgliederLogic();
// Override handleCancelAddUser to use the parent's onAddUserClose
const handleCancelAddUserOverride = () => {
onAddUserClose?.();
};
// Configure edit form fields - moved inside component to access editingUser
const editFields: EditFieldConfig[] = [
{
key: 'username',
label: t('users.column.username', 'Username'),
type: 'string',
editable: false, // Username should not be editable
required: true
},
{
key: 'fullName',
label: t('users.column.name', 'Name'),
type: 'string',
editable: true,
required: true
},
{
key: 'email',
label: t('users.column.email', 'Email'),
type: 'email',
editable: editingUser?.authenticationAuthority === 'local',
required: true
},
{
key: 'language',
label: t('users.column.language', 'Language'),
type: 'enum',
editable: true,
required: true,
options: ['en', 'de', 'fr']
},
{
key: 'privilege',
label: t('users.column.privilege', 'Privilege'),
type: 'enum',
editable: true,
required: true,
options: ['viewer', 'user', 'admin', 'sysadmin']
},
{
key: 'enabled',
label: t('users.column.enabled', 'Enabled'),
type: 'boolean',
editable: true,
required: false
},
{
key: 'authenticationAuthority',
label: t('users.column.authAuthority', 'Auth Authority'),
type: 'readonly',
editable: false,
required: false
}
];
// Configure add user form fields
const addUserFields: EditFieldConfig[] = [
{
key: 'username',
label: t('users.column.username', 'Username'),
type: 'string',
editable: true,
required: true
},
{
key: 'email',
label: t('users.column.email', 'Email'),
type: 'email',
editable: true,
required: true
},
{
key: 'password',
label: t('users.column.password', 'Password'),
type: 'string',
editable: true,
required: true,
placeholder: t('users.password.placeholder', 'Enter password')
},
{
key: 'fullName',
label: t('users.column.name', 'Name'),
type: 'string',
editable: true,
required: true
},
{
key: 'language',
label: t('users.column.language', 'Language'),
type: 'enum',
editable: true,
required: true,
options: ['en', 'de', 'fr']
},
{
key: 'privilege',
label: t('users.column.privilege', 'Privilege'),
type: 'enum',
editable: true,
required: true,
options: ['viewer', 'user', 'admin', 'sysadmin']
},
{
key: 'enabled',
label: t('users.column.enabled', 'Enabled'),
type: 'boolean',
editable: true,
required: false
}
];
if (error) { if (error) {
return ( return (
<div className={styles.errorState}> <div className={styles.errorState}>
@ -43,7 +168,95 @@ function MitgliederTable({ className = '' }: MitgliederTableProps) {
actions={actions} actions={actions}
onRefresh={refetch} onRefresh={refetch}
className={styles.mitgliederFormGenerator} className={styles.mitgliederFormGenerator}
getRowDataAttributes={(row) => {
const enabled = row.enabled?.toString() || 'false';
console.log('Row data attributes:', { enabled, row: row.username });
return {
'user-enabled': enabled
};
}}
/> />
{/* Edit User Popup */}
{editingUser && (
<Popup
isOpen={!!editingUser}
title={t('users.edit.title', 'Edit User')}
onClose={handleCancelEdit}
size="medium"
>
<EditForm
data={editingUser}
fields={editFields}
onSave={handleSaveUser}
onCancel={handleCancelEdit}
saveButtonText={t('common.save', 'Save')}
cancelButtonText={t('common.cancel', 'Cancel')}
/>
</Popup>
)}
{/* Delete Confirmation Popup */}
{deletingUser && (
<Popup
isOpen={!!deletingUser}
title={t('users.delete.title', 'Delete User')}
onClose={handleCancelDelete}
size="small"
actions={[
{
label: t('common.cancel', 'Cancel'),
onClick: handleCancelDelete,
variant: 'secondary'
},
{
label: t('users.delete.confirm', 'Delete'),
onClick: handleConfirmDelete,
variant: 'danger'
}
]}
>
<div className={styles.deleteConfirmation}>
<p>{t('users.delete.message', 'Are you sure you want to delete this user?')}</p>
<div className={styles.userInfo}>
<strong>{t('users.column.name', 'Name')}:</strong> {deletingUser.fullName || deletingUser.username}
</div>
<div className={styles.userInfo}>
<strong>{t('users.column.email', 'Email')}:</strong> {deletingUser.email}
</div>
<p className={styles.warning}>
{t('users.delete.warning', 'This action cannot be undone.')}
</p>
</div>
</Popup>
)}
{/* Add User Popup */}
{showAddUser && (
<Popup
isOpen={showAddUser}
title={t('users.add.title', 'Add User')}
onClose={handleCancelAddUserOverride}
size="medium"
>
<EditForm
data={{
username: '',
email: '',
password: '',
fullName: '',
language: 'en',
privilege: 'user',
enabled: true
}}
fields={addUserFields}
onSave={handleSaveNewUser}
onCancel={handleCancelAddUserOverride}
saveButtonText={t('users.add.create', 'Create User')}
cancelButtonText={t('common.cancel', 'Cancel')}
/>
</Popup>
)}
</div> </div>
); );
} }

View file

@ -1,4 +1,6 @@
import { useMemo } from 'react'; import { useMemo, useState } from 'react';
import { MdModeEdit } from 'react-icons/md';
import { IoIosTrash } from 'react-icons/io';
import { useOrgUsers } from '../../hooks/useUsers'; import { useOrgUsers } from '../../hooks/useUsers';
import { useLanguage } from '../../contexts/LanguageContext'; import { useLanguage } from '../../contexts/LanguageContext';
@ -10,8 +12,10 @@ import type {
} from './mitgliederTypes'; } from './mitgliederTypes';
export function useMitgliederLogic(): MitgliederLogicReturn { export function useMitgliederLogic(): MitgliederLogicReturn {
const { users, loading, error, refetch } = useOrgUsers(); const { users, loading, error, refetch, updateUser, deleteUser, createUser } = useOrgUsers();
const { t } = useLanguage(); const { t } = useLanguage();
const [editingUser, setEditingUser] = useState<any>(null);
const [deletingUser, setDeletingUser] = useState<any>(null);
// Configure columns for the users table // Configure columns for the users table
const columns: UserColumnConfig[] = useMemo(() => [ const columns: UserColumnConfig[] = useMemo(() => [
@ -85,11 +89,157 @@ export function useMitgliederLogic(): MitgliederLogicReturn {
</span> </span>
); );
} }
},
{
key: 'privilege',
label: t('users.column.privilege', 'Privilege'),
type: 'enum',
width: 120,
minWidth: 100,
maxWidth: 150,
sortable: true,
filterable: true,
filterOptions: ['viewer', 'user', 'admin', 'sysadmin'],
formatter: (value: string | undefined) => {
const privilegeMap: Record<string, string> = {
'viewer': t('users.privilege.viewer', 'Viewer'),
'user': t('users.privilege.user', 'User'),
'admin': t('users.privilege.admin', 'Admin'),
'sysadmin': t('users.privilege.sysadmin', 'Sysadmin')
};
return (
<span className="userPrivilege">
{value ? privilegeMap[value] || value : t('users.noPrivilege', 'No Privilege')}
</span>
);
}
},
{
key: 'enabled',
label: t('users.column.enabled', 'Enabled'),
type: 'boolean',
width: 100,
minWidth: 80,
maxWidth: 120,
sortable: true,
filterable: true,
formatter: (value: boolean | undefined) => (
<span className={`userEnabled ${value ? 'enabled' : 'disabled'}`}>
{value ? t('users.enabled.yes', 'Yes') : t('users.enabled.no', 'No')}
</span>
)
},
{
key: 'authenticationAuthority',
label: t('users.column.authAuthority', 'Auth Authority'),
type: 'enum',
width: 150,
minWidth: 120,
maxWidth: 200,
sortable: true,
filterable: true,
filterOptions: ['local', 'msft'],
formatter: (value: string | undefined) => {
const authMap: Record<string, string> = {
'local': t('users.auth.local', 'Local'),
'msft': t('users.auth.msft', 'Microsoft')
};
return (
<span className="userAuthAuthority">
{value ? authMap[value] || value : t('users.noAuthAuthority', 'No Auth Authority')}
</span>
);
}
} }
], [t]); ], [t]);
// Configure action buttons (empty for now) // Handle edit user
const actions: UserActionConfig[] = useMemo(() => [], []); const handleEditUser = (user: any) => {
setEditingUser(user);
};
// Handle save user
const handleSaveUser = async (updatedUser: any) => {
try {
await updateUser(updatedUser.id, updatedUser);
setEditingUser(null);
} catch (error) {
console.error('Failed to update user:', error);
}
};
// Handle cancel edit
const handleCancelEdit = () => {
setEditingUser(null);
};
// Handle delete user
const handleDeleteUser = (user: any) => {
setDeletingUser(user);
};
// Handle confirm delete
const handleConfirmDelete = async () => {
if (deletingUser) {
try {
await deleteUser(deletingUser.id);
setDeletingUser(null);
} catch (error) {
console.error('Failed to delete user:', error);
}
}
};
// Handle cancel delete
const handleCancelDelete = () => {
setDeletingUser(null);
};
// Handle save new user
const handleSaveNewUser = async (userData: any) => {
try {
// Create user data with required fields
const newUserData = {
username: userData.username,
email: userData.email,
password: userData.password,
fullName: userData.fullName,
language: userData.language,
enabled: userData.enabled || false,
privilege: userData.privilege,
authenticationAuthority: 'local' // New users are always local
};
// Debug logging
console.log('Creating user with data:', newUserData);
console.log('Password field:', userData.password);
console.log('Password type:', typeof userData.password);
console.log('Password length:', userData.password?.length);
await createUser(newUserData);
} catch (error) {
console.error('Failed to create user:', error);
}
};
// Handle cancel add user
const handleCancelAddUser = () => {
// This will be handled by the parent component
};
// Configure action buttons
const actions: UserActionConfig[] = useMemo(() => [
{
label: t('users.actions.edit', 'Edit'),
icon: () => <MdModeEdit />,
onClick: (row: any) => handleEditUser(row)
},
{
label: t('users.actions.delete', 'Delete'),
icon: () => <IoIosTrash />,
onClick: (row: any) => handleDeleteUser(row)
}
], [t]);
return { return {
// Data // Data
@ -102,6 +252,22 @@ export function useMitgliederLogic(): MitgliederLogicReturn {
// Additional data for rendering // Additional data for rendering
columns, columns,
actions actions,
// Edit functionality
editingUser,
setEditingUser,
handleSaveUser,
handleCancelEdit,
// Delete functionality
deletingUser,
setDeletingUser,
handleConfirmDelete,
handleCancelDelete,
// Add user functionality
handleSaveNewUser,
handleCancelAddUser
}; };
} }

View file

@ -4,6 +4,8 @@ import { User } from '../../hooks/useUsers';
// Props for the MitgliederTable component // Props for the MitgliederTable component
export interface MitgliederTableProps { export interface MitgliederTableProps {
className?: string; className?: string;
showAddUser?: boolean;
onAddUserClose?: () => void;
} }
// Action configuration for user actions // Action configuration for user actions
@ -41,4 +43,20 @@ export interface MitgliederLogicReturn {
// Additional data for rendering // Additional data for rendering
columns: UserColumnConfig[]; columns: UserColumnConfig[];
actions: UserActionConfig[]; actions: UserActionConfig[];
// Edit functionality
editingUser: User | null;
setEditingUser: (user: User | null) => void;
handleSaveUser: (updatedUser: User) => Promise<void>;
handleCancelEdit: () => void;
// Delete functionality
deletingUser: User | null;
setDeletingUser: (user: User | null) => void;
handleConfirmDelete: () => Promise<void>;
handleCancelDelete: () => void;
// Add user functionality
handleSaveNewUser: (userData: any) => Promise<void>;
handleCancelAddUser: () => void;
} }

View file

@ -0,0 +1,128 @@
# Privilege System Implementation Summary
## Overview
A comprehensive privilege checking system has been implemented for the Page Manager that integrates with the backend user data while maintaining localStorage for specific features like speech signup.
## Key Features
### 1. 4-Level Privilege System
- **viewer**: Basic read-only access
- **user**: Standard user access
- **admin**: Administrative access
- **sysadmin**: System administrator access
### 2. Backend Integration
- User privilege data comes from the backend via `/api/local/me` endpoint
- User data is cached in localStorage for performance
- Privilege checkers use real user data from the backend
### 3. Page Access Control
- **All pages visible to all privilege levels**: dashboard, dateien, prompts, connections, workflows, einstellungen, speech
- **Admin-only pages**: team-bereich (only visible to admin and sysadmin)
- **Feature-specific access**: speech/transcripts (requires speech signup in localStorage)
## Implementation Details
### Files Modified
#### `src/hooks/privilegeCheckers.ts`
- Updated to use real user privilege data from localStorage cache
- Maintains localStorage-based checkers for features not yet integrated with backend
- Added comprehensive privilege checker functions
#### `src/hooks/useUsers.ts`
- Added localStorage caching of user data for privilege checkers
- Improved initialization to load cached data first, then fetch fresh data
- Maintains existing logout functionality
#### `src/components/PageManager/pageConfigInterface.ts`
- Added `privilegeChecker` attribute for main page access control
- Added `subpagePrivilegeChecker` attribute for subpage access control
#### `src/components/PageManager/pageConfigs.ts`
- Updated all page configurations with appropriate privilege checkers
- Set team-bereich to admin-only access
- Set all other pages to viewer-level access (all users)
### Privilege Checkers Available
```typescript
// Role-based checkers (using backend user data)
privilegeCheckers.viewerRole // viewer, user, admin, sysadmin
privilegeCheckers.userRole // user, admin, sysadmin
privilegeCheckers.adminRole // admin, sysadmin
privilegeCheckers.sysadminRole // sysadmin only
// Feature-based checkers (using localStorage)
privilegeCheckers.speechSignup // Speech signup (localStorage)
privilegeCheckers.authenticated // Any authenticated user
privilegeCheckers.alwaysAllow // Always visible
privilegeCheckers.neverAllow // Never visible
```
## Usage Examples
### Setting User Privilege for Testing
```typescript
import { simulateUserPrivilege, testAllPagesVisibility } from '../hooks/privilegeTestUtils';
// Simulate a user with admin privilege
simulateUserPrivilege('admin');
// Test all pages visibility
await testAllPagesVisibility();
```
### Adding Privilege to New Pages
```typescript
{
path: 'new-page',
component: NewPage,
// ... other config
privilegeChecker: privilegeCheckers.adminRole, // Only admins can access
}
```
## Data Flow
1. **User Login**: Backend returns user data with privilege level
2. **Caching**: User data is cached in localStorage as 'currentUser'
3. **Privilege Checking**: Privilege checkers read from localStorage cache
4. **Page Visibility**: getSidebarItems() checks privileges and filters visible pages
5. **Real-time Updates**: User data is refreshed on each app load
## Testing
Use the provided test utilities in `src/hooks/privilegeTestUtils.ts`:
```typescript
// Test all privilege checkers
await testAllPrivilegeCheckers();
// Simulate different privilege levels
simulateUserPrivilege('viewer');
simulateUserPrivilege('user');
simulateUserPrivilege('admin');
simulateUserPrivilege('sysadmin');
// Test specific page visibility
await checkPageVisibility('team-bereich');
// Test all pages
await testAllPagesVisibility();
```
## Security Notes
- Client-side privilege checking is for UI/UX purposes only
- Backend must enforce actual access control for sensitive operations
- User data is cached locally but refreshed on each app load
- Speech signup remains in localStorage as it's not yet backend-integrated
## Future Enhancements
- Real-time privilege updates without page refresh
- Server-side privilege validation
- More granular permission levels
- Privilege inheritance and delegation
- Audit logging for privilege changes

View file

@ -0,0 +1,245 @@
# Generic Subpage System
This system provides a flexible, reusable way to create pages with conditional subpages in the sidebar. It replaces the hardcoded speech-specific logic with a generic approach that can be applied to any page.
## Key Features
- **Generic subpage support**: Any page can have subpages by setting `hasSubpages: true`
- **Flexible privilege checking**: Multiple built-in privilege checkers plus support for custom ones
- **Automatic submenu generation**: Subpages are automatically found and organized into submenus
- **Conditional display**: Subpages only show when privilege conditions are met
- **Async support**: Privilege checkers can be async for complex logic
## How It Works
### 1. Page Configuration
To create a page with subpages, add these properties to your `PageConfig`:
```typescript
{
path: 'parent-page',
component: YourComponent,
// ... other properties
// Generic subpage support
hasSubpages: true, // Enable subpage support
subpagePrefix: 'parent-page', // Prefix to identify subpages
privilegeChecker: privilegeCheckers.yourChecker, // When to show subpages
}
```
### 2. Subpage Configuration
Subpages are regular page configs with these requirements:
```typescript
{
path: 'parent-page/subpage', // Must start with parent prefix
component: SubpageComponent,
// ... other properties
showInSidebar: false, // Important: prevents main sidebar display
}
```
### 3. Privilege Checkers
The system includes several built-in privilege checkers:
#### LocalStorage with Expiration
```typescript
privilegeCheckers.speechSignup // 24-hour localStorage data
privilegeCheckers.premiumUser // 30-day localStorage data
```
#### Role-Based
```typescript
privilegeCheckers.adminRole // Check for admin/super-admin roles
```
#### Feature Flags
```typescript
privilegeCheckers.betaFeatures // Check feature flag status
```
#### Authentication
```typescript
privilegeCheckers.authenticated // Check if user is logged in
```
#### Custom Checkers
```typescript
const customChecker = createCustomPrivilegeChecker(async () => {
// Your custom logic
return someCondition;
});
```
## Examples
### Example 1: Admin Section
```typescript
// Main admin page
{
path: 'admin',
component: AdminDashboard,
id: 'admin',
name: 'Admin',
icon: AdminIcon,
order: 10,
showInSidebar: true,
hasSubpages: true,
subpagePrefix: 'admin',
privilegeChecker: privilegeCheckers.adminRole,
}
// Admin subpages
{
path: 'admin/users',
component: AdminUsers,
id: 'admin-users',
name: 'User Management',
showInSidebar: false, // Important!
},
{
path: 'admin/settings',
component: AdminSettings,
id: 'admin-settings',
name: 'System Settings',
showInSidebar: false, // Important!
}
```
### Example 2: Premium Features
```typescript
// Main premium page
{
path: 'premium',
component: PremiumDashboard,
id: 'premium',
name: 'Premium',
icon: PremiumIcon,
order: 11,
showInSidebar: true,
hasSubpages: true,
subpagePrefix: 'premium',
privilegeChecker: privilegeCheckers.premiumUser,
}
// Premium subpages
{
path: 'premium/advanced-tools',
component: AdvancedTools,
id: 'premium-advanced',
name: 'Advanced Tools',
showInSidebar: false,
},
{
path: 'premium/priority-support',
component: PrioritySupport,
id: 'premium-support',
name: 'Priority Support',
showInSidebar: false,
}
```
## Migration from Hardcoded System
The old hardcoded speech system has been replaced with the generic system:
### Before (Hardcoded)
```typescript
// Special handling in getSidebarItems()
if (config.path === 'speech') {
const hasSignedUp = checkSpeechSignUpStatus();
// ... hardcoded logic
}
```
### After (Generic)
```typescript
// Speech page config
{
path: 'speech',
component: Speech,
hasSubpages: true,
subpagePrefix: 'speech',
privilegeChecker: privilegeCheckers.speechSignup,
}
// Speech subpage
{
path: 'speech/transcripts',
component: SpeechTranscripts,
showInSidebar: false,
}
```
## Adding New Subpages
1. **Create the main page config** with subpage support:
```typescript
{
path: 'your-page',
hasSubpages: true,
subpagePrefix: 'your-page',
privilegeChecker: privilegeCheckers.yourChecker,
}
```
2. **Create subpage configs**:
```typescript
{
path: 'your-page/subpage1',
component: Subpage1Component,
showInSidebar: false,
},
{
path: 'your-page/subpage2',
component: Subpage2Component,
showInSidebar: false,
}
```
3. **Add to pageConfigs array** - the system will automatically handle the rest!
## Benefits
- **No more hardcoded logic**: Each page handles its own subpage logic
- **Reusable privilege checkers**: Common patterns are built-in
- **Easy to extend**: Adding new subpages is just configuration
- **Type-safe**: Full TypeScript support
- **Async support**: Complex privilege logic can be async
- **Error handling**: Graceful fallbacks when privilege checks fail
## API Reference
### PageConfig Properties
- `hasSubpages?: boolean` - Enable subpage support
- `subpagePrefix?: string` - Prefix to identify subpages
- `privilegeChecker?: PrivilegeChecker` - Function to check privileges
### PrivilegeChecker Type
```typescript
type PrivilegeChecker = () => boolean | Promise<boolean>;
```
### Built-in Privilege Checkers
- `privilegeCheckers.speechSignup` - 24-hour localStorage data
- `privilegeCheckers.premiumUser` - 30-day localStorage data
- `privilegeCheckers.adminRole` - Admin role check
- `privilegeCheckers.betaFeatures` - Feature flag check
- `privilegeCheckers.authenticated` - Authentication check
### Helper Functions
- `createLocalStoragePrivilegeChecker(dataKey, timestampKey, expirationHours)` - Create localStorage-based checker
- `createRolePrivilegeChecker(requiredRoles, getUserRoles)` - Create role-based checker
- `createFeatureFlagChecker(featureFlag, getFeatureFlags)` - Create feature flag checker
- `createCustomPrivilegeChecker(checkFunction)` - Create custom checker

View file

@ -1,5 +1,9 @@
import React from 'react'; import React from 'react';
import { IconType } from 'react-icons'; import { IconType } from 'react-icons';
import { SidebarSubmenuItemData } from '../Sidebar/sidebarTypes';
// Generic privilege checker function type
export type PrivilegeChecker = () => boolean | Promise<boolean>;
// Extended page configuration interface that includes sidebar properties // Extended page configuration interface that includes sidebar properties
export interface PageConfig { export interface PageConfig {
@ -22,7 +26,15 @@ export interface PageConfig {
order?: number; // For sidebar ordering order?: number; // For sidebar ordering
showInSidebar?: boolean; // Whether to show in sidebar (default: true) showInSidebar?: boolean; // Whether to show in sidebar (default: true)
// Subpages support // Privilege checking
privilegeChecker?: PrivilegeChecker; // Function to check if user has access to this page
// Generic subpage support
hasSubpages?: boolean; // Whether this page can have subpages
subpagePrefix?: string; // Prefix to identify subpages (e.g., 'speech' for 'speech/transcripts')
subpagePrivilegeChecker?: PrivilegeChecker; // Function to check if subpages should be shown
// Subpages support (legacy - for direct subpage definitions)
subpages?: PageConfig[]; subpages?: PageConfig[];
// Lifecycle hooks // Lifecycle hooks

View file

@ -1,4 +1,5 @@
import { PageConfig, SidebarItem } from './pageConfigInterface'; import { PageConfig, SidebarItem } from './pageConfigInterface';
import { privilegeCheckers } from '../../hooks/privilegeCheckers';
import { lazy } from 'react'; import { lazy } from 'react';
// Import icons for sidebar // Import icons for sidebar
@ -35,6 +36,8 @@ export const pageConfigs: PageConfig[] = [
icon: LuTicket, icon: LuTicket,
order: 1, order: 1,
showInSidebar: true, showInSidebar: true,
// All privilege levels can access dashboard
privilegeChecker: privilegeCheckers.viewerRole,
onActivate: async () => { onActivate: async () => {
if (import.meta.env.DEV) console.log('Dashboard activated - state preserved'); if (import.meta.env.DEV) console.log('Dashboard activated - state preserved');
// You can add analytics tracking here // You can add analytics tracking here
@ -55,6 +58,8 @@ export const pageConfigs: PageConfig[] = [
icon: FaRegFileAlt, icon: FaRegFileAlt,
order: 2, order: 2,
showInSidebar: true, showInSidebar: true,
// All privilege levels can access dateien
privilegeChecker: privilegeCheckers.viewerRole,
onActivate: async () => { onActivate: async () => {
if (import.meta.env.DEV) console.log('Dateien activated'); if (import.meta.env.DEV) console.log('Dateien activated');
}, },
@ -77,6 +82,8 @@ export const pageConfigs: PageConfig[] = [
icon: LuMessageSquareText , icon: LuMessageSquareText ,
order: 3, order: 3,
showInSidebar: true, showInSidebar: true,
// All privilege levels can access prompts
privilegeChecker: privilegeCheckers.viewerRole,
onActivate: async () => { onActivate: async () => {
if (import.meta.env.DEV) console.log('Prompts activated'); if (import.meta.env.DEV) console.log('Prompts activated');
}, },
@ -99,6 +106,8 @@ export const pageConfigs: PageConfig[] = [
icon: MdOutlineWorkOutline, icon: MdOutlineWorkOutline,
order: 5, order: 5,
showInSidebar: true, showInSidebar: true,
// Privilege checking - only admin and sysadmin can access team management
privilegeChecker: privilegeCheckers.adminRole,
onActivate: async () => { onActivate: async () => {
if (import.meta.env.DEV) console.log('Team Bereich activated'); if (import.meta.env.DEV) console.log('Team Bereich activated');
} }
@ -115,6 +124,8 @@ export const pageConfigs: PageConfig[] = [
icon: FaPlug, icon: FaPlug,
order: 4, order: 4,
showInSidebar: true, showInSidebar: true,
// All privilege levels can access connections
privilegeChecker: privilegeCheckers.viewerRole,
onActivate: async () => { onActivate: async () => {
if (import.meta.env.DEV) console.log('Connections activated'); if (import.meta.env.DEV) console.log('Connections activated');
}, },
@ -134,6 +145,8 @@ export const pageConfigs: PageConfig[] = [
icon: LuWorkflow, icon: LuWorkflow,
order: 3, order: 3,
showInSidebar: true, showInSidebar: true,
// All privilege levels can access workflows
privilegeChecker: privilegeCheckers.viewerRole,
onActivate: async () => { onActivate: async () => {
if (import.meta.env.DEV) console.log('Workflows activated - preserving workflow state'); if (import.meta.env.DEV) console.log('Workflows activated - preserving workflow state');
}, },
@ -159,6 +172,8 @@ export const pageConfigs: PageConfig[] = [
icon: GoGear, icon: GoGear,
order: 6, order: 6,
showInSidebar: true, showInSidebar: true,
// All privilege levels can access settings
privilegeChecker: privilegeCheckers.viewerRole,
onActivate: async () => { onActivate: async () => {
if (import.meta.env.DEV) console.log('Einstellungen activated'); if (import.meta.env.DEV) console.log('Einstellungen activated');
} }
@ -175,6 +190,12 @@ export const pageConfigs: PageConfig[] = [
icon: FaRegFileAlt, icon: FaRegFileAlt,
order: 7, order: 7,
showInSidebar: true, showInSidebar: true,
// All privilege levels can access speech
privilegeChecker: privilegeCheckers.viewerRole,
// Generic subpage support
hasSubpages: true,
subpagePrefix: 'speech',
subpagePrivilegeChecker: privilegeCheckers.speechSignup,
onActivate: async () => { onActivate: async () => {
if (import.meta.env.DEV) console.log('Speech activated'); if (import.meta.env.DEV) console.log('Speech activated');
} }
@ -191,6 +212,8 @@ export const pageConfigs: PageConfig[] = [
icon: IoIosDocument, icon: IoIosDocument,
order: 8, order: 8,
showInSidebar: false, // Will be shown as subpage under Speech showInSidebar: false, // Will be shown as subpage under Speech
// Privilege checking - only users with speech signup can access transcripts
privilegeChecker: privilegeCheckers.speechSignup,
onActivate: async () => { onActivate: async () => {
if (import.meta.env.DEV) console.log('Speech Transcripts activated'); if (import.meta.env.DEV) console.log('Speech Transcripts activated');
} }
@ -230,23 +253,47 @@ export const getPageConfig = (path: string): PageConfig | undefined => {
}; };
// Get sidebar items from page configs // Get sidebar items from page configs
export const getSidebarItems = () => { export const getSidebarItems = async () => {
const items: SidebarItem[] = []; const items: SidebarItem[] = [];
pageConfigs // Process each page config
for (const config of pageConfigs
.filter(config => config.showInSidebar !== false) .filter(config => config.showInSidebar !== false)
.sort((a, b) => (a.order || 0) - (b.order || 0)) .sort((a, b) => (a.order || 0) - (b.order || 0))) {
.forEach(config => {
// Check if this is the Speech item and if user has signed up
if (config.path === 'speech') {
const hasSignedUp = checkSpeechSignUpStatus();
if (hasSignedUp) { // Check if user has privilege to access this page
// Find the transcript subpage let hasPagePrivilege = true;
const transcriptConfig = pageConfigs.find(c => c.path === 'speech/transcripts'); if (config.privilegeChecker) {
try {
hasPagePrivilege = await config.privilegeChecker();
console.log(`🔍 Page privilege check for ${config.path}:`, { hasPagePrivilege });
} catch (error) {
console.error(`Error checking page privilege for ${config.path}:`, error);
hasPagePrivilege = false;
}
}
if (transcriptConfig) { // Skip this page if user doesn't have privilege
// Create expandable Speech item with submenu if (!hasPagePrivilege) {
console.log(`❌ Skipping ${config.path} - no privilege`);
continue;
}
// Check if this page has subpages and should show them
if (config.hasSubpages && config.subpagePrefix && config.subpagePrivilegeChecker) {
try {
const hasSubpagePrivilege = await config.subpagePrivilegeChecker();
if (hasSubpagePrivilege) {
// Find all subpages for this parent
const subpages = pageConfigs.filter(c =>
c.path.startsWith(`${config.subpagePrefix}/`) &&
c.path !== config.subpagePrefix &&
c.showInSidebar === false // Subpages should not show as main items
);
if (subpages.length > 0) {
// Create expandable item with submenu
items.push({ items.push({
id: config.id, id: config.id,
name: config.name, name: config.name,
@ -254,16 +301,14 @@ export const getSidebarItems = () => {
icon: config.icon, icon: config.icon,
moduleEnabled: config.moduleEnabled ?? true, moduleEnabled: config.moduleEnabled ?? true,
order: config.order || 0, order: config.order || 0,
submenu: [ submenu: subpages.map(subpage => ({
{ id: subpage.id,
id: transcriptConfig.id, name: subpage.name,
name: transcriptConfig.name, link: `/${subpage.path}`
link: `/${transcriptConfig.path}` }))
}
]
}); });
} else { } else {
// Fallback to regular item if transcript config not found // No subpages found, show as regular item
items.push({ items.push({
id: config.id, id: config.id,
name: config.name, name: config.name,
@ -274,7 +319,7 @@ export const getSidebarItems = () => {
}); });
} }
} else { } else {
// User hasn't signed up, show regular non-expandable item // No subpage privilege, show as regular non-expandable item
items.push({ items.push({
id: config.id, id: config.id,
name: config.name, name: config.name,
@ -284,8 +329,9 @@ export const getSidebarItems = () => {
order: config.order || 0 order: config.order || 0
}); });
} }
} else { } catch (error) {
// Regular non-Speech items console.error(`Error checking subpage privilege for ${config.path}:`, error);
// Fallback to regular item on error
items.push({ items.push({
id: config.id, id: config.id,
name: config.name, name: config.name,
@ -295,40 +341,21 @@ export const getSidebarItems = () => {
order: config.order || 0 order: config.order || 0
}); });
} }
}); } else {
// Regular items without subpages
items.push({
id: config.id,
name: config.name,
link: `/${config.path}`,
icon: config.icon,
moduleEnabled: config.moduleEnabled ?? true,
order: config.order || 0
});
}
}
return items; return items;
}; };
// Helper function to check speech sign-up status
const checkSpeechSignUpStatus = (): boolean => {
try {
const savedData = localStorage.getItem('speechSignUpData');
const timestamp = localStorage.getItem('speechSignUpTimestamp');
console.log('🔍 Checking speech sign-up status:', { savedData: !!savedData, timestamp });
if (savedData && timestamp) {
const signUpTime = parseInt(timestamp);
const now = Date.now();
const hoursDiff = (now - signUpTime) / (1000 * 60 * 60);
console.log('📊 Speech sign-up validation:', {
signUpTime,
now,
hoursDiff,
isValid: hoursDiff < 24
});
// Data is valid for 24 hours
return hoursDiff < 24;
}
console.log('❌ No speech sign-up data found');
return false;
} catch (error) {
console.error('Error checking speech sign-up status:', error);
return false;
}
};
export default pageConfigs; export default pageConfigs;

View file

@ -0,0 +1,234 @@
// Example configurations showing how to add subpages with the new generic system
import { PageConfig } from './pageConfigInterface';
import { privilegeCheckers, createCustomPrivilegeChecker } from '../../hooks/privilegeCheckers';
import { lazy } from 'react';
// Example 1: Admin section with multiple subpages
export const adminPageConfig: PageConfig = {
path: 'admin',
component: lazy(() => import('../../pages/Home/Admin')),
persistent: false,
preload: true,
moduleEnabled: true,
id: 'admin',
name: 'Admin',
icon: () => null, // Your admin icon
order: 10,
showInSidebar: true,
// Generic subpage support
hasSubpages: true,
subpagePrefix: 'admin',
privilegeChecker: privilegeCheckers.adminRole, // Only show subpages to admins
onActivate: async () => {
console.log('Admin section activated');
}
};
// Example 2: Premium features with expiration-based access
export const premiumPageConfig: PageConfig = {
path: 'premium',
component: lazy(() => import('../../pages/Home/Premium')),
persistent: false,
preload: true,
moduleEnabled: true,
id: 'premium',
name: 'Premium',
icon: () => null, // Your premium icon
order: 11,
showInSidebar: true,
// Generic subpage support
hasSubpages: true,
subpagePrefix: 'premium',
privilegeChecker: privilegeCheckers.premiumUser, // Only show subpages to premium users
onActivate: async () => {
console.log('Premium section activated');
}
};
// Example 3: Beta features with feature flag
export const betaPageConfig: PageConfig = {
path: 'beta',
component: lazy(() => import('../../pages/Home/Beta')),
persistent: false,
preload: false,
moduleEnabled: true,
id: 'beta',
name: 'Beta Features',
icon: () => null, // Your beta icon
order: 12,
showInSidebar: true,
// Generic subpage support
hasSubpages: true,
subpagePrefix: 'beta',
privilegeChecker: privilegeCheckers.betaFeatures, // Only show subpages if beta features are enabled
onActivate: async () => {
console.log('Beta section activated');
}
};
// Example 4: Custom privilege checker
const customPrivilegeChecker = createCustomPrivilegeChecker(async () => {
// Your custom logic here
const userLevel = localStorage.getItem('userLevel');
const hasSpecialAccess = userLevel === 'vip' || userLevel === 'premium';
console.log('Custom privilege check:', { userLevel, hasSpecialAccess });
return hasSpecialAccess;
});
export const customPageConfig: PageConfig = {
path: 'custom',
component: lazy(() => import('../../pages/Home/Custom')),
persistent: false,
preload: true,
moduleEnabled: true,
id: 'custom',
name: 'Custom Features',
icon: () => null, // Your custom icon
order: 13,
showInSidebar: true,
// Generic subpage support
hasSubpages: true,
subpagePrefix: 'custom',
privilegeChecker: customPrivilegeChecker,
onActivate: async () => {
console.log('Custom section activated');
}
};
// Example subpage configurations
export const exampleSubpages: PageConfig[] = [
// Admin subpages
{
path: 'admin/users',
component: lazy(() => import('../../pages/Home/AdminUsers')),
persistent: false,
preload: false,
moduleEnabled: true,
id: 'admin-users',
name: 'User Management',
icon: () => null,
order: 1,
showInSidebar: false, // Important: subpages should not show as main items
onActivate: async () => {
console.log('Admin Users activated');
}
},
{
path: 'admin/settings',
component: lazy(() => import('../../pages/Home/AdminSettings')),
persistent: false,
preload: false,
moduleEnabled: true,
id: 'admin-settings',
name: 'System Settings',
icon: () => null,
order: 2,
showInSidebar: false,
onActivate: async () => {
console.log('Admin Settings activated');
}
},
{
path: 'admin/analytics',
component: lazy(() => import('../../pages/Home/AdminAnalytics')),
persistent: false,
preload: false,
moduleEnabled: true,
id: 'admin-analytics',
name: 'Analytics',
icon: () => null,
order: 3,
showInSidebar: false,
onActivate: async () => {
console.log('Admin Analytics activated');
}
},
// Premium subpages
{
path: 'premium/advanced-tools',
component: lazy(() => import('../../pages/Home/PremiumAdvancedTools')),
persistent: false,
preload: false,
moduleEnabled: true,
id: 'premium-advanced',
name: 'Advanced Tools',
icon: () => null,
order: 1,
showInSidebar: false,
onActivate: async () => {
console.log('Premium Advanced Tools activated');
}
},
{
path: 'premium/priority-support',
component: lazy(() => import('../../pages/Home/PremiumSupport')),
persistent: false,
preload: false,
moduleEnabled: true,
id: 'premium-support',
name: 'Priority Support',
icon: () => null,
order: 2,
showInSidebar: false,
onActivate: async () => {
console.log('Premium Support activated');
}
},
// Beta subpages
{
path: 'beta/experimental-features',
component: lazy(() => import('../../pages/Home/BetaExperimental')),
persistent: false,
preload: false,
moduleEnabled: true,
id: 'beta-experimental',
name: 'Experimental Features',
icon: () => null,
order: 1,
showInSidebar: false,
onActivate: async () => {
console.log('Beta Experimental Features activated');
}
},
// Custom subpages
{
path: 'custom/special-tools',
component: lazy(() => import('../../pages/Home/CustomSpecialTools')),
persistent: false,
preload: false,
moduleEnabled: true,
id: 'custom-special',
name: 'Special Tools',
icon: () => null,
order: 1,
showInSidebar: false,
onActivate: async () => {
console.log('Custom Special Tools activated');
}
}
];
// How to add these to your main pageConfigs array:
/*
// In your main pageConfigs.ts file, you would add:
// 1. Add the main page configs
...adminPageConfig,
...premiumPageConfig,
...betaPageConfig,
...customPageConfig,
// 2. Add the subpage configs
...exampleSubpages,
// The system will automatically:
// - Detect pages with hasSubpages: true
// - Find subpages with matching subpagePrefix
// - Check privileges using the privilegeChecker
// - Show/hide submenus accordingly
*/

View file

@ -295,12 +295,7 @@ export function usePromptsLogic(): PromptsLogicReturn {
/> />
); );
}, },
onClick: (row: Prompt) => { // onClick is handled by FormGenerator for delete confirmation
const isDeletable = isPromptDeletable(row);
if (isDeletable && !deletingPrompts.has(row.id)) {
handleDeletePrompt(row);
}
}
}, },
], [t, deletingPrompts, handleDeletePrompt, handleEditPrompt, handleCopyPrompt]); ], [t, deletingPrompts, handleDeletePrompt, handleEditPrompt, handleCopyPrompt]);

View file

@ -19,7 +19,7 @@ export interface PromptsTableProps {
export interface PromptActionConfig { export interface PromptActionConfig {
label: string | ((row: Prompt) => string); label: string | ((row: Prompt) => string);
icon: (row: Prompt) => React.ReactElement; icon: (row: Prompt) => React.ReactElement;
onClick: (row: Prompt) => void; onClick?: (row: Prompt) => void;
} }
// Column configuration for the prompts table // Column configuration for the prompts table

View file

@ -11,6 +11,9 @@ import { GoSidebarExpand, GoSidebarCollapse } from 'react-icons/go';
const Sidebar: React.FC<SidebarProps> = ({ data }) => { const Sidebar: React.FC<SidebarProps> = ({ data }) => {
const sidebar = useSidebarLogic(); const sidebar = useSidebarLogic();
// Ensure data is always an array
const sidebarItems = Array.isArray(data) ? data : [];
return ( return (
<div <div
className={`${styles.sidebarContainer} ${sidebar.state.isMinimized ? styles.minimized : ''}`} className={`${styles.sidebarContainer} ${sidebar.state.isMinimized ? styles.minimized : ''}`}
@ -40,7 +43,7 @@ const Sidebar: React.FC<SidebarProps> = ({ data }) => {
<div <div
className={styles.sidebar} className={styles.sidebar}
> >
{data.map(item => { {sidebarItems.map(item => {
return ( return (
<SidebarItem <SidebarItem
key={item.id} key={item.id}
@ -62,7 +65,28 @@ const Sidebar: React.FC<SidebarProps> = ({ data }) => {
} }
const SidebarWithData: React.FC = () => { const SidebarWithData: React.FC = () => {
const sidebarData = useSidebarFromPageConfigs(); const { items: sidebarData, isLoading } = useSidebarFromPageConfigs();
if (isLoading) {
return (
<div className={`${styles.sidebarContainer}`}>
<div className={styles.logoContainer}>
<div className={styles.logoWrapper}>
<div className={styles.logoText}>
<span className={styles.logoPower}>Power</span>
<span className={styles.logoOn}>On</span>
</div>
</div>
</div>
<div className={styles.sidebar}>
<div style={{ padding: '20px', textAlign: 'center' }}>
Loading...
</div>
</div>
</div>
);
}
return <Sidebar data={sidebarData} />; return <Sidebar data={sidebarData} />;
}; };

View file

@ -29,7 +29,7 @@ export interface TestSharepointTableProps {
// Table Action Interface // Table Action Interface
export interface TableAction { export interface TableAction {
label: string; label: string;
onClick: (document: SharePointDocument) => Promise<void> | void; onClick?: (document: SharePointDocument) => Promise<void> | void;
icon: React.ReactNode | ((document: SharePointDocument) => React.ReactNode); icon: React.ReactNode | ((document: SharePointDocument) => React.ReactNode);
} }

View file

@ -417,12 +417,8 @@ export function useWorkflowsLogic(): WorkflowsLogicReturn {
label: t('workflows.action.delete'), label: t('workflows.action.delete'),
icon: (_row: Workflow) => { icon: (_row: Workflow) => {
return <IoIosTrash />; return <IoIosTrash />;
},
onClick: (row: Workflow) => {
if (!deletingWorkflows.has(row.id)) {
handleDeleteWorkflow(row);
}
} }
// onClick is handled by FormGenerator for delete confirmation
}, },
], [t, deletingWorkflows, handleDeleteWorkflow, handleEditWorkflow, handlePlayWorkflow]); ], [t, deletingWorkflows, handleDeleteWorkflow, handleEditWorkflow, handlePlayWorkflow]);

View file

@ -41,7 +41,7 @@ export interface WorkflowsLogicReturn {
export interface WorkflowActionConfig { export interface WorkflowActionConfig {
label: string; label: string;
icon: (row: Workflow) => React.ReactElement; icon: (row: Workflow) => React.ReactElement;
onClick: (row: Workflow) => void; onClick?: (row: Workflow) => void;
} }
export interface WorkflowColumnConfig { export interface WorkflowColumnConfig {

View file

@ -0,0 +1,168 @@
/* User Information Form Styles */
.userInfoForm {
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px;
background: var(--color-bg);
border-radius: 25px;
border: 1px solid var(--color-primary);
margin-bottom: 20px;
}
.formRow {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.formField {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-width: 250px;
}
.fieldLabel {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text);
font-family: var(--font-family);
}
.fieldNote {
font-size: 0.75rem;
font-weight: 400;
color: var(--color-primary);
font-style: italic;
}
.formInput,
.formSelect {
padding: 12px 16px;
border-radius: 25px;
border: 1px solid var(--color-primary);
background: var(--color-bg);
color: var(--color-text);
font-family: var(--font-family);
font-size: 0.875rem;
transition: all 0.3s ease;
outline: none;
}
.formInput:focus,
.formSelect:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(63, 81, 181, 0.1);
}
.formInput:hover,
.formSelect:hover {
border-color: var(--color-secondary);
}
.formInput[readonly] {
background: var(--color-gray-light);
cursor: not-allowed;
opacity: 0.7;
}
.formSelect option {
background: var(--color-bg);
color: var(--color-text);
padding: 10px;
}
.formActions {
display: flex;
justify-content: flex-end;
padding-top: 10px;
}
.saveButton {
padding: 12px 24px;
border-radius: 25px;
border: none;
background: var(--color-secondary);
color: white;
cursor: pointer;
transition: all 0.3s ease;
font-family: var(--font-family);
font-size: 0.875rem;
font-weight: 500;
min-width: 120px;
}
.saveButton:hover:not(:disabled) {
background: var(--color-secondary);
border-color: var(--color-secondary);
box-shadow: 0 4px 12px rgba(63, 81, 181, 0.3);
}
.saveButton:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.updateMessage {
padding: 12px 16px;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 10px;
}
.successMessage {
background-color: #e8f5e8;
color: #2e7d32;
border: 1px solid #81c784;
}
.errorMessage {
background-color: #fce4ec;
color: #c2185b;
border: 1px solid #f48fb1;
padding: 12px 16px;
border-radius: 12px;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 20px;
}
.settingLabel {
font-size: 1rem;
font-weight: 500;
color: var(--color-text);
font-family: var(--font-family);
}
.settingDescription {
font-size: 0.875rem;
color: var(--color-primary);
font-family: var(--font-family);
}
/* Responsive design */
@media (max-width: 768px) {
.formRow {
flex-direction: column;
gap: 15px;
}
.formField {
min-width: unset;
}
.formActions {
justify-content: center;
}
.saveButton {
width: 100%;
max-width: 200px;
}
}

View file

@ -0,0 +1,388 @@
import { useState, useEffect, useRef } from 'react';
import { useLanguage, Language } from '../../contexts/LanguageContext';
import { useCurrentUser, useUser, User } from '../../hooks/useUsers';
import styles from './settingsUser.module.css';
// Add a check to ensure we're in the right context
const isClient = typeof window !== 'undefined';
interface SettingsUserProps {
className?: string;
}
function SettingsUser({ className }: SettingsUserProps) {
console.log('👤 SettingsUser component loaded');
const { currentLanguage, setLanguage, t } = useLanguage();
const { user: currentUser } = useCurrentUser();
const { getUser, updateUser, isLoading: updateLoading } = useUser();
// Local state for user data fetched directly via API
const [user, setUser] = useState<User | null>(null);
const [userLoading, setUserLoading] = useState(false);
const [userError, setUserError] = useState<string | null>(null);
// Form state for user info
const [userForm, setUserForm] = useState({
username: '',
fullName: '',
email: '',
language: 'en' as Language,
privilege: '',
enabled: true
});
// Phone name state (stored in localStorage only)
const [phoneName, setPhoneName] = useState('');
const [updateMessage, setUpdateMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const [isUpdating, setIsUpdating] = useState(false); // Flag to prevent form reset during update
const hasLoadedUser = useRef(false);
// Load phone name from localStorage
const loadPhoneName = () => {
try {
const savedPhoneName = localStorage.getItem('userPhoneName');
if (savedPhoneName) {
setPhoneName(savedPhoneName);
}
} catch (error) {
console.error('Failed to load phone name from localStorage:', error);
}
};
// Save phone name to localStorage
const savePhoneName = (name: string) => {
try {
if (name.trim()) {
localStorage.setItem('userPhoneName', name.trim());
} else {
localStorage.removeItem('userPhoneName');
}
} catch (error) {
console.error('Failed to save phone name to localStorage:', error);
}
};
// Fetch user data directly using the /api/users/{userId} endpoint
const fetchUserData = async () => {
if (!currentUser?.id || hasLoadedUser.current) return;
hasLoadedUser.current = true;
setUserLoading(true);
setUserError(null);
try {
const userData = await getUser(currentUser.id);
setUser(userData);
} catch (error) {
console.error('Failed to fetch user data:', error);
setUserError(typeof error === 'string' ? error : 'Failed to load user data');
hasLoadedUser.current = false; // Reset on error to allow retry
} finally {
setUserLoading(false);
}
};
// Load phone name from localStorage on component mount
useEffect(() => {
loadPhoneName();
}, []);
// Fetch user data when currentUser is available
useEffect(() => {
if (currentUser?.id && !hasLoadedUser.current) {
fetchUserData();
}
}, [currentUser?.id]);
// Update form when user data is loaded (but not during an active update)
useEffect(() => {
if (user && !isUpdating) {
console.log('🔄 Updating form with user data:', user);
setUserForm({
username: user.username || '',
fullName: user.fullName || '',
email: user.email || '',
language: (user.language as Language) || currentLanguage,
privilege: user.privilege || '',
enabled: user.enabled
});
}
}, [user, currentLanguage, isUpdating]);
const handleUserFormChange = (field: keyof typeof userForm, value: string | boolean) => {
setUserForm(prev => ({ ...prev, [field]: value }));
setUpdateMessage(null); // Clear any previous messages
// Language change will be handled when the form is submitted, not immediately
};
const handlePhoneNameChange = (value: string) => {
setPhoneName(value);
savePhoneName(value); // Save to localStorage immediately
};
const handleSaveUserInfo = async () => {
if (!user) return;
setIsUpdating(true); // Prevent form reset during update
try {
// Create complete User object with updated form data
// Only include editable fields based on authentication authority
const completeUserData: User = {
id: user.id,
username: user.authenticationAuthority === 'local' ? userForm.username : user.username,
fullName: user.authenticationAuthority === 'local' ? userForm.fullName : user.fullName,
email: user.authenticationAuthority === 'local' ? userForm.email : user.email,
language: userForm.language, // Language is always editable
privilege: userForm.privilege,
enabled: userForm.enabled,
authenticationAuthority: user.authenticationAuthority,
mandateId: user.mandateId
};
// Update user via API - this returns the updated user
const updatedUser = await updateUser(user.id, completeUserData);
if (updatedUser) {
console.log('✅ User update successful:', updatedUser);
// Update local user state with the returned data
setUser(updatedUser);
// Update frontend language if it was changed
const newLanguage = updatedUser.language as Language;
if (newLanguage && newLanguage !== currentLanguage) {
try {
await setLanguage(newLanguage);
console.log('🌍 Frontend language updated to:', newLanguage);
} catch (error) {
console.error('Failed to change frontend language:', error);
}
}
// Success: Update form with the actual returned data to ensure consistency
setUserForm({
username: updatedUser.username || '',
fullName: updatedUser.fullName || '',
email: updatedUser.email || '',
language: newLanguage || currentLanguage,
privilege: updatedUser.privilege || '',
enabled: updatedUser.enabled
});
console.log('📝 Form updated with new data');
// Dispatch event to notify other components (like sidebar) that user data was updated
window.dispatchEvent(new CustomEvent('userInfoUpdated'));
setUpdateMessage({ type: 'success', text: t('settings.userinfo.success') });
} else {
throw new Error('No updated user data returned from server');
}
// Clear message after 3 seconds
setTimeout(() => setUpdateMessage(null), 3000);
} catch (error) {
console.error('Failed to update user info:', error);
setUpdateMessage({ type: 'error', text: t('settings.userinfo.update_error') });
// Reset form to original user data on error
if (user) {
setUserForm({
username: user.username || '',
fullName: user.fullName || '',
email: user.email || '',
language: (user.language as Language) || currentLanguage,
privilege: user.privilege || '',
enabled: user.enabled
});
}
} finally {
setIsUpdating(false); // Re-enable form sync
}
};
const getLanguageLabel = (lang: Language): string => {
switch (lang) {
case 'de': return t('language.german');
case 'en': return t('language.english');
case 'fr': return t('language.french');
default: return lang;
}
};
if (userLoading) {
return (
<div className={`${styles.userInfoForm} ${className || ''}`}>
<div style={{ padding: '2rem', textAlign: 'center' }}>
{t('settings.userinfo.loading')}
</div>
</div>
);
}
return (
<div className={`${styles.userInfoForm} ${className || ''}`}>
{userError && (
<div className={styles.errorMessage}>
{t('settings.userinfo.error')}: {typeof userError === 'string' ? userError : 'An error occurred'}
</div>
)}
{user && (
<>
<span className={styles.settingLabel}>{t('settings.userinfo')}</span>
<span className={styles.settingDescription}>
{t('settings.userinfo.description')}
</span>
<div className={styles.formRow}>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.userinfo.username')}
{user.authenticationAuthority !== 'local' && (
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
)}
</label>
<input
type="text"
className={styles.formInput}
value={userForm.username}
onChange={(e) => handleUserFormChange('username', e.target.value)}
placeholder={t('settings.userinfo.username')}
readOnly={user.authenticationAuthority !== 'local'}
title={user.authenticationAuthority !== 'local' ?
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
undefined}
/>
</div>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.userinfo.fullname')}
{user.authenticationAuthority !== 'local' && (
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
)}
</label>
<input
type="text"
className={styles.formInput}
value={userForm.fullName}
onChange={(e) => handleUserFormChange('fullName', e.target.value)}
placeholder={t('settings.userinfo.fullname')}
readOnly={user.authenticationAuthority !== 'local'}
title={user.authenticationAuthority !== 'local' ?
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
undefined}
/>
</div>
</div>
<div className={styles.formRow}>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.userinfo.email')}
{user.authenticationAuthority !== 'local' && (
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
)}
</label>
<input
type="email"
className={styles.formInput}
value={userForm.email}
onChange={(e) => handleUserFormChange('email', e.target.value)}
placeholder={t('settings.userinfo.email')}
readOnly={user.authenticationAuthority !== 'local'}
title={user.authenticationAuthority !== 'local' ?
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
undefined}
/>
</div>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.userinfo.phone_name')}
<span className={styles.fieldNote}>({t('settings.userinfo.phone_name.description')})</span>
</label>
<input
type="text"
className={styles.formInput}
value={phoneName}
onChange={(e) => handlePhoneNameChange(e.target.value)}
placeholder={t('settings.userinfo.phone_name')}
title="This field is saved locally and not sent to the server"
/>
</div>
</div>
<div className={styles.formRow}>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.language')}
<span className={styles.fieldNote}>({t('settings.language.description')})</span>
</label>
<select
className={styles.formSelect}
value={userForm.language}
onChange={(e) => handleUserFormChange('language', e.target.value)}
aria-label={t('settings.language')}
>
<option value="de">{getLanguageLabel('de')}</option>
<option value="en">{getLanguageLabel('en')}</option>
<option value="fr">{getLanguageLabel('fr')}</option>
</select>
</div>
<div className={styles.formField}>
{/* Empty field to maintain layout */}
</div>
</div>
<div className={styles.formRow}>
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('settings.userinfo.privilege')}</label>
<input
type="text"
className={styles.formInput}
value={userForm.privilege}
readOnly
placeholder={t('settings.userinfo.privilege')}
title="This field is read-only"
/>
</div>
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('settings.userinfo.auth_authority')}</label>
<input
type="text"
className={styles.formInput}
value={user.authenticationAuthority}
readOnly
placeholder={t('settings.userinfo.auth_authority')}
title="This field is read-only"
/>
</div>
</div>
{updateMessage && (
<div className={`${styles.updateMessage} ${updateMessage.type === 'success' ? styles.successMessage : styles.errorMessage}`}>
{updateMessage.text}
</div>
)}
<div className={styles.formActions}>
<button
className={styles.saveButton}
onClick={handleSaveUserInfo}
disabled={updateLoading}
>
{updateLoading ? t('settings.userinfo.saving') : t('settings.userinfo.save')}
</button>
</div>
</>
)}
</div>
);
}
export default SettingsUser;

View file

@ -0,0 +1,229 @@
import { PrivilegeChecker } from '../components/PageManager/pageConfigInterface';
// Function to get current user privilege from localStorage (where it's cached)
const getCurrentUserPrivilege = (): string | null => {
try {
const userData = localStorage.getItem('currentUser');
console.log('🔍 Raw user data from localStorage:', userData);
if (userData) {
const user = JSON.parse(userData);
console.log('🔍 Parsed user object:', user);
console.log('🔍 User privilege:', user.privilege);
return user.privilege || null;
}
console.log('❌ No user data found in localStorage');
return null;
} catch (error) {
console.error('Error getting user privilege from localStorage:', error);
return null;
}
};
// Generic privilege checker for localStorage-based data with expiration
export const createLocalStoragePrivilegeChecker = (
dataKey: string,
timestampKey: string,
expirationHours: number = 24
): PrivilegeChecker => {
return (): boolean => {
try {
const savedData = localStorage.getItem(dataKey);
const timestamp = localStorage.getItem(timestampKey);
console.log(`🔍 Checking privilege for ${dataKey}:`, {
savedData: !!savedData,
timestamp
});
if (savedData && timestamp) {
const dataTime = parseInt(timestamp);
const now = Date.now();
const hoursDiff = (now - dataTime) / (1000 * 60 * 60);
console.log(`📊 Privilege validation for ${dataKey}:`, {
dataTime,
now,
hoursDiff,
isValid: hoursDiff < expirationHours
});
return hoursDiff < expirationHours;
}
console.log(`❌ No privilege data found for ${dataKey}`);
return false;
} catch (error) {
console.error(`Error checking privilege for ${dataKey}:`, error);
return false;
}
};
};
// Generic privilege checker for user roles/permissions
export const createRolePrivilegeChecker = (
requiredRoles: string[],
getUserRoles: () => string[] | Promise<string[]>
): PrivilegeChecker => {
return async (): Promise<boolean> => {
try {
const userRoles = await getUserRoles();
const hasRequiredRole = requiredRoles.some(role => userRoles.includes(role));
console.log(`🔍 Checking role privilege:`, {
requiredRoles,
userRoles,
hasRequiredRole
});
return hasRequiredRole;
} catch (error) {
console.error('Error checking role privilege:', error);
return false;
}
};
};
// Generic privilege checker for feature flags
export const createFeatureFlagChecker = (
featureFlag: string,
getFeatureFlags: () => Record<string, boolean> | Promise<Record<string, boolean>>
): PrivilegeChecker => {
return async (): Promise<boolean> => {
try {
const flags = await getFeatureFlags();
const isEnabled = flags[featureFlag] === true;
console.log(`🔍 Checking feature flag ${featureFlag}:`, {
isEnabled,
allFlags: flags
});
return isEnabled;
} catch (error) {
console.error(`Error checking feature flag ${featureFlag}:`, error);
return false;
}
};
};
// Generic privilege checker for authentication status
export const createAuthPrivilegeChecker = (
isAuthenticated: () => boolean | Promise<boolean>
): PrivilegeChecker => {
return async (): Promise<boolean> => {
try {
const authenticated = await isAuthenticated();
console.log(`🔍 Checking authentication status:`, { authenticated });
return authenticated;
} catch (error) {
console.error('Error checking authentication status:', error);
return false;
}
};
};
// Helper function to create custom privilege checkers
export const createCustomPrivilegeChecker = (
checkFunction: () => boolean | Promise<boolean>
): PrivilegeChecker => {
return checkFunction;
};
// Predefined privilege checkers for common use cases
export const privilegeCheckers = {
// Speech signup checker (existing functionality)
speechSignup: createLocalStoragePrivilegeChecker(
'speechSignUpData',
'speechSignUpTimestamp',
24
),
// Admin role checker - for admin and sysadmin users
adminRole: createRolePrivilegeChecker(
['admin', 'sysadmin'],
() => {
const userPrivilege = getCurrentUserPrivilege();
console.log('🔍 Admin role check - user privilege:', userPrivilege);
console.log('🔍 Admin role check - required roles: [admin, sysadmin]');
console.log('🔍 Admin role check - user roles array:', userPrivilege ? [userPrivilege] : []);
return Promise.resolve(userPrivilege ? [userPrivilege] : []);
}
),
// Sysadmin role checker - for sysadmin only
sysadminRole: createRolePrivilegeChecker(
['sysadmin'],
() => {
const userPrivilege = getCurrentUserPrivilege();
return Promise.resolve(userPrivilege ? [userPrivilege] : []);
}
),
// Premium user checker
premiumUser: createLocalStoragePrivilegeChecker(
'premiumUserData',
'premiumUserTimestamp',
24 * 30 // 30 days
),
// Feature flag checker
betaFeatures: createFeatureFlagChecker(
'betaFeatures',
() => {
// This would typically come from your feature flag service
const flags = JSON.parse(localStorage.getItem('featureFlags') || '{}');
return Promise.resolve(flags);
}
),
// Authentication checker
authenticated: createAuthPrivilegeChecker(
() => {
// This would typically come from your auth context
const token = localStorage.getItem('authToken');
return Promise.resolve(!!token);
}
),
// User role checker - for user, admin, and sysadmin access
userRole: createRolePrivilegeChecker(
['user', 'admin', 'sysadmin'],
() => {
const userPrivilege = getCurrentUserPrivilege();
return Promise.resolve(userPrivilege ? [userPrivilege] : []);
}
),
// Viewer role checker - for viewer, user, admin, and sysadmin access (all levels)
viewerRole: createRolePrivilegeChecker(
['viewer', 'user', 'admin', 'sysadmin'],
() => {
const userPrivilege = getCurrentUserPrivilege();
console.log('🔍 Viewer role check - user privilege:', userPrivilege);
console.log('🔍 Viewer role check - required roles: [viewer, user, admin, sysadmin]');
console.log('🔍 Viewer role check - user roles array:', userPrivilege ? [userPrivilege] : []);
return Promise.resolve(userPrivilege ? [userPrivilege] : []);
}
),
// Subscription checker - for paid features
hasSubscription: createLocalStoragePrivilegeChecker(
'subscriptionData',
'subscriptionTimestamp',
24 * 7 // 7 days
),
// Mandate checker - for users who have submitted their mandate
hasMandate: createLocalStoragePrivilegeChecker(
'mandateData',
'mandateTimestamp',
24 * 30 // 30 days
),
// Always allow access (for public pages)
alwaysAllow: createCustomPrivilegeChecker(() => true),
// Never allow access (for disabled features)
neverAllow: createCustomPrivilegeChecker(() => false)
};

View file

@ -0,0 +1,114 @@
// Utility functions for testing and debugging the privilege system
import { privilegeCheckers } from './privilegeCheckers';
// Function to test all privilege checkers
export const testAllPrivilegeCheckers = async () => {
console.log('🧪 Testing all privilege checkers...');
const results = {
speechSignup: await privilegeCheckers.speechSignup(),
adminRole: await privilegeCheckers.adminRole(),
sysadminRole: await privilegeCheckers.sysadminRole(),
userRole: await privilegeCheckers.userRole(),
viewerRole: await privilegeCheckers.viewerRole(),
authenticated: await privilegeCheckers.authenticated(),
alwaysAllow: await privilegeCheckers.alwaysAllow(),
neverAllow: await privilegeCheckers.neverAllow()
};
console.log('📊 Privilege checker results:', results);
return results;
};
// Function to simulate different user privilege levels for testing
export const simulateUserPrivilege = (privilege: string) => {
const testUser = {
id: 'test-user',
username: 'testuser',
email: 'test@example.com',
fullName: 'Test User',
language: 'en',
enabled: true,
privilege: privilege,
authenticationAuthority: 'local',
mandateId: 'test-mandate'
};
localStorage.setItem('currentUser', JSON.stringify(testUser));
console.log(`🎭 Simulated user with privilege: ${privilege}`);
};
// Function to clear test data
export const clearTestData = () => {
localStorage.removeItem('currentUser');
localStorage.removeItem('speechSignUpData');
localStorage.removeItem('speechSignUpTimestamp');
console.log('🧹 Cleared test data');
};
// Function to get current user privilege for debugging
export const getCurrentUserPrivilege = (): string | null => {
try {
const userData = localStorage.getItem('currentUser');
if (userData) {
const user = JSON.parse(userData);
return user.privilege || null;
}
return null;
} catch (error) {
console.error('Error getting user privilege:', error);
return null;
}
};
// Function to check if a specific page should be visible
export const checkPageVisibility = async (pagePath: string) => {
const privilegeMap: Record<string, any> = {
'dashboard': privilegeCheckers.viewerRole,
'dateien': privilegeCheckers.viewerRole,
'prompts': privilegeCheckers.viewerRole,
'team-bereich': privilegeCheckers.adminRole,
'connections': privilegeCheckers.viewerRole,
'workflows': privilegeCheckers.viewerRole,
'einstellungen': privilegeCheckers.viewerRole,
'speech': privilegeCheckers.viewerRole,
'speech/transcripts': privilegeCheckers.speechSignup
};
const checker = privilegeMap[pagePath];
if (!checker) {
console.log(`❌ No privilege checker found for page: ${pagePath}`);
return false;
}
const hasAccess = await checker();
console.log(`🔍 Page ${pagePath} access:`, hasAccess);
return hasAccess;
};
// Function to test all pages visibility
export const testAllPagesVisibility = async () => {
console.log('🧪 Testing all pages visibility...');
const pages = [
'dashboard',
'dateien',
'prompts',
'team-bereich',
'connections',
'workflows',
'einstellungen',
'speech',
'speech/transcripts'
];
const results: Record<string, boolean> = {};
for (const page of pages) {
results[page] = await checkPageVisibility(page);
}
console.log('📊 All pages visibility results:', results);
return results;
};

View file

@ -347,6 +347,349 @@ export function useRegister() {
}; };
} }
// Google Authentication
interface GoogleAuthResponse {
accessToken: string;
tokenType: string;
user: {
username: string;
email: string;
fullName: string;
mandateId: number;
};
}
export function useGoogleAuth() {
const [googleError, setGoogleError] = useState<string | null>(null);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const loginWithGoogle = async (): Promise<GoogleAuthResponse> => {
setIsGoogleLoading(true);
setGoogleError(null);
try {
return new Promise((resolve, reject) => {
const backendUrl = getApiBaseUrl();
const loginUrl = `${backendUrl}/api/google/login`;
console.log('🔐 Starting Google authentication...');
console.log('🌐 Backend URL:', backendUrl);
console.log('🔗 Login URL:', loginUrl);
// First, get the Google login URL from the backend using fetch to avoid CORS issues
fetch(`${backendUrl}/api/google/login`, {
method: 'GET',
mode: 'cors',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
redirect: 'manual' // Don't follow redirects
})
.then(response => {
console.log('📡 Backend response:', response);
console.log('📊 Response status:', response.status);
console.log('📊 Response type:', response.type);
console.log('📊 Response headers:', response.headers);
// Check if it's a redirect response
if (response.status === 0 || response.type === 'opaque') {
// This might be a CORS issue, try to get the redirect URL from the response
console.log('🔄 CORS/Redirect detected, trying to extract URL from response');
// Try to read the response as text to get the redirect URL
return response.text().then(text => {
console.log('📄 Response text:', text);
// Look for redirect URL in the response
const urlMatch = text.match(/https:\/\/accounts\.google\.com\/o\/oauth2\/auth[^"'\s]*/);
if (urlMatch) {
return { login_url: urlMatch[0] };
}
// If no URL found in text, try to construct it from the error
throw new Error('Could not extract Google OAuth URL from response');
});
} else if (response.status >= 200 && response.status < 300) {
// Normal JSON response
return response.json();
} else if (response.status >= 300 && response.status < 400) {
// Redirect response
const location = response.headers.get('location');
console.log('🔄 Redirect location:', location);
if (location) {
return { login_url: location };
}
throw new Error('Redirect response without location header');
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
})
.then(data => {
console.log('📄 Response data:', data);
if (data.login_url) {
// Open popup with the Google login URL
const popup = window.open(
data.login_url,
'google-login',
'width=500,height=600,scrollbars=yes,resizable=yes,top=100,left=100'
);
if (!popup) {
const errorMsg = 'Popup was blocked by browser. Please allow popups for this site and try again.';
console.error('❌ Popup blocked:', errorMsg);
setGoogleError(errorMsg);
setIsGoogleLoading(false);
reject(new Error('Popup was blocked'));
return;
}
console.log('✅ Popup opened successfully');
// Listen for messages from the popup
const messageListener = (event: MessageEvent) => {
console.log('📨 Received message from popup:', event.origin, event.data);
// Verify origin for security
const apiUrl = new URL(backendUrl);
if (event.origin !== apiUrl.origin) {
console.warn('⚠️ Message from unauthorized origin:', event.origin, 'Expected:', apiUrl.origin);
return;
}
if (event.data.type === 'google_auth_success') {
console.log('✅ Google authentication successful');
// Store the auth data with normalized field names
if (event.data.token_data) {
const normalizedTokenData = {
accessToken: event.data.token_data.tokenAccess, // Convert tokenAccess to accessToken
tokenType: event.data.token_data.tokenType,
userId: event.data.token_data.userId,
expiresAt: event.data.token_data.expiresAt,
createdAt: event.data.token_data.createdAt
};
localStorage.setItem('auth_data', JSON.stringify(normalizedTokenData));
console.log('💾 Auth data stored in localStorage');
}
// Clean up
window.removeEventListener('message', messageListener);
popup.close();
setIsGoogleLoading(false);
// Resolve with the token data
resolve({
accessToken: event.data.token_data.tokenAccess,
tokenType: event.data.token_data.tokenType || 'bearer',
user: {
username: '', // Will be populated by the backend
email: '',
fullName: '',
mandateId: 0
}
});
} else if (event.data.type === 'google_connection_error') {
console.error('❌ Google connection error:', event.data.error);
// Handle error
window.removeEventListener('message', messageListener);
popup.close();
setIsGoogleLoading(false);
setGoogleError(event.data.error || 'Google authentication failed');
reject(new Error(event.data.error || 'Google authentication failed'));
}
};
// Add message listener
window.addEventListener('message', messageListener);
// Handle popup closing without completing auth
let popupClosedManually = false;
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
setIsGoogleLoading(false);
if (!popupClosedManually) {
console.warn('⚠️ Popup was closed before authentication completed');
setGoogleError('Authentication was cancelled - popup was closed before completing login');
} else {
console.log(' Popup closed after successful authentication');
}
if (!popupClosedManually) {
reject(new Error('Authentication was cancelled'));
}
}
}, 1000);
// Set a timeout to detect if popup doesn't load
const loadTimeout = setTimeout(() => {
if (!popup.closed) {
console.warn('⚠️ Popup did not load within 60 seconds');
popup.close();
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
setIsGoogleLoading(false);
setGoogleError('Authentication timeout - please check your internet connection and try again');
reject(new Error('Authentication timeout'));
}
}, 60000);
// Override popup.close to mark as manually closed
const originalClose = popup.close;
popup.close = function() {
popupClosedManually = true;
clearTimeout(loadTimeout);
return originalClose.call(this);
};
} else {
throw new Error('No login URL received from backend');
}
})
.catch(error => {
console.error('❌ Failed to get Google login URL:', error);
console.log('🔄 Attempting fallback approach...');
// Fallback: Try to construct the Google OAuth URL directly
// This is a temporary solution until the backend is fixed
const fallbackGoogleUrl = `${backendUrl}/api/google/login`;
console.log('🔄 Using fallback URL:', fallbackGoogleUrl);
// Open popup with the fallback URL (let the backend handle the redirect)
const popup = window.open(
fallbackGoogleUrl,
'google-login',
'width=500,height=600,scrollbars=yes,resizable=yes,top=100,left=100'
);
if (!popup) {
const errorMsg = 'Popup was blocked by browser. Please allow popups for this site and try again.';
console.error('❌ Popup blocked:', errorMsg);
setGoogleError(errorMsg);
setIsGoogleLoading(false);
reject(new Error('Popup was blocked'));
return;
}
console.log('✅ Popup opened successfully with fallback URL');
// Listen for messages from the popup
const messageListener = (event: MessageEvent) => {
console.log('📨 Received message from popup:', event.origin, event.data);
// Verify origin for security
const apiUrl = new URL(backendUrl);
if (event.origin !== apiUrl.origin) {
console.warn('⚠️ Message from unauthorized origin:', event.origin, 'Expected:', apiUrl.origin);
return;
}
if (event.data.type === 'google_auth_success') {
console.log('✅ Google authentication successful');
// Store the auth data with normalized field names
if (event.data.token_data) {
const normalizedTokenData = {
accessToken: event.data.token_data.tokenAccess, // Convert tokenAccess to accessToken
tokenType: event.data.token_data.tokenType,
userId: event.data.token_data.userId,
expiresAt: event.data.token_data.expiresAt,
createdAt: event.data.token_data.createdAt
};
localStorage.setItem('auth_data', JSON.stringify(normalizedTokenData));
console.log('💾 Auth data stored in localStorage');
}
// Clean up
window.removeEventListener('message', messageListener);
popup.close();
setIsGoogleLoading(false);
// Resolve with the token data
resolve({
accessToken: event.data.token_data.tokenAccess,
tokenType: event.data.token_data.tokenType || 'bearer',
user: {
username: '', // Will be populated by the backend
email: '',
fullName: '',
mandateId: 0
}
});
} else if (event.data.type === 'google_connection_error') {
console.error('❌ Google connection error:', event.data.error);
// Handle error
window.removeEventListener('message', messageListener);
popup.close();
setIsGoogleLoading(false);
setGoogleError(event.data.error || 'Google authentication failed');
reject(new Error(event.data.error || 'Google authentication failed'));
}
};
// Add message listener
window.addEventListener('message', messageListener);
// Handle popup closing without completing auth
let popupClosedManually = false;
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
setIsGoogleLoading(false);
if (!popupClosedManually) {
console.warn('⚠️ Popup was closed before authentication completed');
setGoogleError('Authentication was cancelled - popup was closed before completing login');
} else {
console.log(' Popup closed after successful authentication');
}
if (!popupClosedManually) {
reject(new Error('Authentication was cancelled'));
}
}
}, 1000);
// Set a timeout to detect if popup doesn't load
const loadTimeout = setTimeout(() => {
if (!popup.closed) {
console.warn('⚠️ Popup did not load within 60 seconds');
popup.close();
clearInterval(checkClosed);
window.removeEventListener('message', messageListener);
setIsGoogleLoading(false);
setGoogleError('Authentication timeout - please check your internet connection and try again');
reject(new Error('Authentication timeout'));
}
}, 60000);
// Override popup.close to mark as manually closed
const originalClose = popup.close;
popup.close = function() {
popupClosedManually = true;
clearTimeout(loadTimeout);
return originalClose.call(this);
};
});
});
} catch (error: any) {
console.error('❌ Google authentication error:', error);
setGoogleError(error.message || 'Google authentication failed');
setIsGoogleLoading(false);
throw error;
}
};
return {
loginWithGoogle,
error: googleError,
isLoading: isGoogleLoading
};
}
// Microsoft Registration // Microsoft Registration
interface MsalRegisterData { interface MsalRegisterData {
username: string; username: string;

View file

@ -161,6 +161,46 @@ export function useConnections() {
} }
}; };
// Refresh Microsoft token
const refreshMicrosoftToken = async (connectionId: string): Promise<Connection> => {
try {
const data = await request({
url: `/api/connections/${connectionId}/refresh-microsoft-token`,
method: 'post'
});
// Update local state
setConnections(prev =>
prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn)
);
return data;
} catch (error) {
console.error('Error refreshing Microsoft token:', error);
throw error;
}
};
// Refresh Google token
const refreshGoogleToken = async (connectionId: string): Promise<Connection> => {
try {
const data = await request({
url: `/api/connections/${connectionId}/refresh-google-token`,
method: 'post'
});
// Update local state
setConnections(prev =>
prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn)
);
return data;
} catch (error) {
console.error('Error refreshing Google token:', error);
throw error;
}
};
return { return {
connections, connections,
fetchConnections, fetchConnections,
@ -169,6 +209,8 @@ export function useConnections() {
connectService, connectService,
disconnectService, disconnectService,
deleteConnection, deleteConnection,
refreshMicrosoftToken,
refreshGoogleToken,
isLoading, isLoading,
error error
}; };

View file

@ -1,12 +1,14 @@
import { useMemo, useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { getSidebarItems } from '../components/PageManager/pageConfigs'; import { getSidebarItems } from '../components/PageManager/pageConfigs';
import { SidebarItem } from '../components/PageManager/pageConfigInterface'; import { SidebarItem } from '../components/PageManager/pageConfigInterface';
import { useLanguage } from '../contexts/LanguageContext'; import { useLanguage } from '../contexts/LanguageContext';
// Hook to get sidebar items from page configurations // Hook to get sidebar items from page configurations
export const useSidebarFromPageConfigs = (): SidebarItem[] => { export const useSidebarFromPageConfigs = (): { items: SidebarItem[]; isLoading: boolean } => {
const { t } = useLanguage(); const { t } = useLanguage();
const [refreshTrigger, setRefreshTrigger] = useState(0); const [refreshTrigger, setRefreshTrigger] = useState(0);
const [sidebarItems, setSidebarItems] = useState<SidebarItem[]>([]);
const [isLoading, setIsLoading] = useState(true);
// Listen for localStorage changes to refresh sidebar when sign-up status changes // Listen for localStorage changes to refresh sidebar when sign-up status changes
useEffect(() => { useEffect(() => {
@ -32,23 +34,39 @@ export const useSidebarFromPageConfigs = (): SidebarItem[] => {
}; };
}, []); }, []);
return useMemo(() => { // Load sidebar items when refreshTrigger changes
console.log('🔄 Sidebar refreshing, trigger:', refreshTrigger); useEffect(() => {
const sidebarItems = getSidebarItems(); const loadSidebarItems = async () => {
console.log('📋 Sidebar items:', sidebarItems.map(item => ({ try {
name: item.name, setIsLoading(true);
hasSubmenu: !!item.submenu, console.log('🔄 Sidebar refreshing, trigger:', refreshTrigger);
submenuCount: item.submenu?.length || 0
})));
// Map the items with translations const items = await getSidebarItems();
return sidebarItems.map(item => ({ console.log('📋 Sidebar items:', items.map(item => ({
...item, name: item.name,
// You can add translations here later if needed hasSubmenu: !!item.submenu,
// For now, we'll use the names from pageConfigs directly submenuCount: item.submenu?.length || 0
name: getTranslatedName(item.name, t) })));
}));
// Map the items with translations
const translatedItems = items.map(item => ({
...item,
name: getTranslatedName(item.name, t)
}));
setSidebarItems(translatedItems);
} catch (error) {
console.error('Error loading sidebar items:', error);
setSidebarItems([]);
} finally {
setIsLoading(false);
}
};
loadSidebarItems();
}, [t, refreshTrigger]); }, [t, refreshTrigger]);
return { items: sidebarItems, isLoading };
}; };
// Helper function to get translated names // Helper function to get translated names
@ -69,4 +87,10 @@ const getTranslatedName = (name: string, t: (key: string) => string): string =>
return translationMap[name] || name; return translationMap[name] || name;
}; };
// Backward-compatible hook that returns just the items array
export const useSidebarItems = (): SidebarItem[] => {
const { items } = useSidebarFromPageConfigs();
return items;
};
export default useSidebarFromPageConfigs; export default useSidebarFromPageConfigs;

View file

@ -28,8 +28,12 @@ export function useCurrentUser() {
method: 'get' method: 'get'
}); });
setUser(data); setUser(data);
// Cache user data in localStorage for privilege checkers
localStorage.setItem('currentUser', JSON.stringify(data));
} catch (error) { } catch (error) {
setUser(null); setUser(null);
// Clear cached user data on error
localStorage.removeItem('currentUser');
} }
}; };
@ -82,6 +86,19 @@ export function useCurrentUser() {
}; };
useEffect(() => { useEffect(() => {
// Try to load user from localStorage first for faster initial load
const cachedUser = localStorage.getItem('currentUser');
if (cachedUser) {
try {
const userData = JSON.parse(cachedUser);
setUser(userData);
} catch (error) {
console.error('Error parsing cached user data:', error);
localStorage.removeItem('currentUser');
}
}
// Always fetch fresh user data from server
fetchCurrentUser(); fetchCurrentUser();
}, []); }, []);
@ -135,6 +152,15 @@ export function useOrgUsers() {
}); });
}; };
const createUser = async (userData: Omit<User, 'id' | 'mandateId'> & { password: string }) => {
await request({
url: '/api/users',
method: 'post',
data: userData
});
await fetchUsers(); // Refresh the list after creation
};
useEffect(() => { useEffect(() => {
fetchUsers(); fetchUsers();
}, []); }, []);
@ -146,7 +172,8 @@ export function useOrgUsers() {
refetch: fetchUsers, refetch: fetchUsers,
updateUser, updateUser,
deleteUser, deleteUser,
getUser getUser,
createUser
}; };
} }

View file

@ -28,6 +28,8 @@ export default {
'settings.userinfo.username': 'Benutzername', 'settings.userinfo.username': 'Benutzername',
'settings.userinfo.fullname': 'Vollständiger Name', 'settings.userinfo.fullname': 'Vollständiger Name',
'settings.userinfo.email': 'E-Mail-Adresse', 'settings.userinfo.email': 'E-Mail-Adresse',
'settings.userinfo.phone_name': 'Rufname am Telefon',
'settings.userinfo.phone_name.description': 'Wie möchten Sie am Telefon genannt werden?',
'settings.userinfo.language': 'Sprache', 'settings.userinfo.language': 'Sprache',
'settings.userinfo.privilege': 'Berechtigungsstufe', 'settings.userinfo.privilege': 'Berechtigungsstufe',
'settings.userinfo.enabled': 'Kontostatus', 'settings.userinfo.enabled': 'Kontostatus',
@ -153,7 +155,7 @@ export default {
// Connection Actions // Connection Actions
'connections.action.edit': 'Bearbeiten', 'connections.action.edit': 'Bearbeiten',
'connections.action.toggle_connection': 'Verbindung umschalten', 'connections.action.update': 'Aktualisieren',
'connections.action.delete': 'Löschen', 'connections.action.delete': 'Löschen',
// Prompt Modal // Prompt Modal
@ -467,11 +469,36 @@ export default {
'users.column.username': 'Benutzername', 'users.column.username': 'Benutzername',
'users.column.name': 'Name', 'users.column.name': 'Name',
'users.column.email': 'E-Mail', 'users.column.email': 'E-Mail',
'users.column.password': 'Passwort',
'users.column.language': 'Sprache', 'users.column.language': 'Sprache',
'users.column.privilege': 'Berechtigung',
'users.column.enabled': 'Aktiviert',
'users.column.authAuthority': 'Auth-Anbieter',
'users.password.placeholder': 'Passwort eingeben',
'users.noUsername': 'Kein Benutzername', 'users.noUsername': 'Kein Benutzername',
'users.noName': 'Kein Name', 'users.noName': 'Kein Name',
'users.noEmail': 'Keine E-Mail', 'users.noEmail': 'Keine E-Mail',
'users.noLanguage': 'Keine Sprache', 'users.noLanguage': 'Keine Sprache',
'users.noPrivilege': 'Keine Berechtigung',
'users.noAuthAuthority': 'Kein Auth-Anbieter',
'users.privilege.viewer': 'Betrachter',
'users.privilege.user': 'Benutzer',
'users.privilege.admin': 'Administrator',
'users.privilege.sysadmin': 'Systemadministrator',
'users.enabled.yes': 'Ja',
'users.enabled.no': 'Nein',
'users.auth.local': 'Lokal',
'users.auth.msft': 'Microsoft',
'users.actions.edit': 'Bearbeiten',
'users.actions.delete': 'Löschen',
'users.edit.title': 'Benutzer bearbeiten',
'users.add.title': 'Benutzer hinzufügen',
'users.add.button': 'Benutzer hinzufügen',
'users.add.create': 'Benutzer erstellen',
'users.delete.title': 'Benutzer löschen',
'users.delete.message': 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
'users.delete.confirm': 'Löschen',
'users.delete.warning': 'Diese Aktion kann nicht rückgängig gemacht werden.',
'users.action.edit': 'Bearbeiten', 'users.action.edit': 'Bearbeiten',
'users.action.delete': 'Löschen', 'users.action.delete': 'Löschen',
'users.delete.confirm': 'Sind Sie sicher, dass Sie "{name}" löschen möchten?', 'users.delete.confirm': 'Sind Sie sicher, dass Sie "{name}" löschen möchten?',

View file

@ -28,6 +28,8 @@ export default {
'settings.userinfo.username': 'Username', 'settings.userinfo.username': 'Username',
'settings.userinfo.fullname': 'Full Name', 'settings.userinfo.fullname': 'Full Name',
'settings.userinfo.email': 'Email Address', 'settings.userinfo.email': 'Email Address',
'settings.userinfo.phone_name': 'Phone Name',
'settings.userinfo.phone_name.description': 'How would you like to be called on the phone?',
'settings.userinfo.language': 'Language', 'settings.userinfo.language': 'Language',
'settings.userinfo.privilege': 'Privilege Level', 'settings.userinfo.privilege': 'Privilege Level',
'settings.userinfo.enabled': 'Account Status', 'settings.userinfo.enabled': 'Account Status',
@ -153,7 +155,7 @@ export default {
// Connection Actions // Connection Actions
'connections.action.edit': 'Edit', 'connections.action.edit': 'Edit',
'connections.action.toggle_connection': 'Toggle Connection', 'connections.action.update': 'Update',
'connections.action.delete': 'Delete', 'connections.action.delete': 'Delete',
@ -467,11 +469,36 @@ export default {
'users.column.username': 'Username', 'users.column.username': 'Username',
'users.column.name': 'Name', 'users.column.name': 'Name',
'users.column.email': 'Email', 'users.column.email': 'Email',
'users.column.password': 'Password',
'users.column.language': 'Language', 'users.column.language': 'Language',
'users.column.privilege': 'Privilege',
'users.column.enabled': 'Enabled',
'users.column.authAuthority': 'Auth Authority',
'users.password.placeholder': 'Enter password',
'users.noUsername': 'No Username', 'users.noUsername': 'No Username',
'users.noName': 'No Name', 'users.noName': 'No Name',
'users.noEmail': 'No Email', 'users.noEmail': 'No Email',
'users.noLanguage': 'No Language', 'users.noLanguage': 'No Language',
'users.noPrivilege': 'No Privilege',
'users.noAuthAuthority': 'No Auth Authority',
'users.privilege.viewer': 'Viewer',
'users.privilege.user': 'User',
'users.privilege.admin': 'Admin',
'users.privilege.sysadmin': 'Sysadmin',
'users.enabled.yes': 'Yes',
'users.enabled.no': 'No',
'users.auth.local': 'Local',
'users.auth.msft': 'Microsoft',
'users.actions.edit': 'Edit',
'users.actions.delete': 'Delete',
'users.edit.title': 'Edit User',
'users.add.title': 'Add User',
'users.add.button': 'Add User',
'users.add.create': 'Create User',
'users.delete.title': 'Delete User',
'users.delete.message': 'Are you sure you want to delete this user?',
'users.delete.confirm': 'Delete',
'users.delete.warning': 'This action cannot be undone.',
'users.action.edit': 'Edit', 'users.action.edit': 'Edit',
'users.action.delete': 'Delete', 'users.action.delete': 'Delete',
'users.delete.confirm': 'Are you sure you want to delete "{name}"?', 'users.delete.confirm': 'Are you sure you want to delete "{name}"?',

View file

@ -28,6 +28,8 @@ export default {
'settings.userinfo.username': 'Nom d\'utilisateur', 'settings.userinfo.username': 'Nom d\'utilisateur',
'settings.userinfo.fullname': 'Nom complet', 'settings.userinfo.fullname': 'Nom complet',
'settings.userinfo.email': 'Adresse e-mail', 'settings.userinfo.email': 'Adresse e-mail',
'settings.userinfo.phone_name': 'Nom au téléphone',
'settings.userinfo.phone_name.description': 'Comment souhaitez-vous être appelé au téléphone ?',
'settings.userinfo.language': 'Langue', 'settings.userinfo.language': 'Langue',
'settings.userinfo.privilege': 'Niveau de privilège', 'settings.userinfo.privilege': 'Niveau de privilège',
'settings.userinfo.enabled': 'Statut du compte', 'settings.userinfo.enabled': 'Statut du compte',
@ -153,7 +155,7 @@ export default {
// Connection Actions // Connection Actions
'connections.action.edit': 'Modifier', 'connections.action.edit': 'Modifier',
'connections.action.toggle_connection': 'Basculer la connexion', 'connections.action.update': 'Mettre à jour',
'connections.action.delete': 'Supprimer', 'connections.action.delete': 'Supprimer',
// Prompt Modal // Prompt Modal
@ -467,11 +469,36 @@ export default {
'users.column.username': 'Nom d\'utilisateur', 'users.column.username': 'Nom d\'utilisateur',
'users.column.name': 'Nom', 'users.column.name': 'Nom',
'users.column.email': 'E-mail', 'users.column.email': 'E-mail',
'users.column.password': 'Mot de passe',
'users.column.language': 'Langue', 'users.column.language': 'Langue',
'users.column.privilege': 'Privilège',
'users.column.enabled': 'Activé',
'users.column.authAuthority': 'Autorité d\'authentification',
'users.password.placeholder': 'Entrez le mot de passe',
'users.noUsername': 'Aucun nom d\'utilisateur', 'users.noUsername': 'Aucun nom d\'utilisateur',
'users.noName': 'Aucun nom', 'users.noName': 'Aucun nom',
'users.noEmail': 'Aucun e-mail', 'users.noEmail': 'Aucun e-mail',
'users.noLanguage': 'Aucune langue', 'users.noLanguage': 'Aucune langue',
'users.noPrivilege': 'Aucun privilège',
'users.noAuthAuthority': 'Aucune autorité d\'authentification',
'users.privilege.viewer': 'Observateur',
'users.privilege.user': 'Utilisateur',
'users.privilege.admin': 'Administrateur',
'users.privilege.sysadmin': 'Administrateur système',
'users.enabled.yes': 'Oui',
'users.enabled.no': 'Non',
'users.auth.local': 'Local',
'users.auth.msft': 'Microsoft',
'users.actions.edit': 'Modifier',
'users.actions.delete': 'Supprimer',
'users.edit.title': 'Modifier l\'utilisateur',
'users.add.title': 'Ajouter un utilisateur',
'users.add.button': 'Ajouter un utilisateur',
'users.add.create': 'Créer l\'utilisateur',
'users.delete.title': 'Supprimer l\'utilisateur',
'users.delete.message': 'Êtes-vous sûr de vouloir supprimer cet utilisateur ?',
'users.delete.confirm': 'Supprimer',
'users.delete.warning': 'Cette action ne peut pas être annulée.',
'users.action.edit': 'Modifier', 'users.action.edit': 'Modifier',
'users.action.delete': 'Supprimer', 'users.action.delete': 'Supprimer',
'users.delete.confirm': 'Êtes-vous sûr de vouloir supprimer "{name}" ?', 'users.delete.confirm': 'Êtes-vous sûr de vouloir supprimer "{name}" ?',

View file

@ -1,60 +1,20 @@
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import styles from './HomeStyles/Einstellungen.module.css'; import styles from './HomeStyles/Einstellungen.module.css';
import sharedStyles from '../../components/PageManager/pages.module.css'; import sharedStyles from '../../components/PageManager/pages.module.css';
import { useLanguage, Language } from '../../contexts/LanguageContext'; import { useLanguage } from '../../contexts/LanguageContext';
import { useCurrentUser, useUser, User } from '../../hooks/useUsers';
import SettingsSpeech from '../../components/settings/settingsSpeech'; import SettingsSpeech from '../../components/settings/settingsSpeech';
import SettingsUser from '../../components/settings/settingsUser';
function Einstellungen() { function Einstellungen() {
console.log('🏠 Einstellungen component loaded'); console.log('🏠 Einstellungen component loaded');
const [isDarkMode, setIsDarkMode] = useState(false); const [isDarkMode, setIsDarkMode] = useState(false);
const { currentLanguage, setLanguage, t, isLoading } = useLanguage(); const { t, isLoading } = useLanguage();
const { user: currentUser, isLoading: currentUserLoading } = useCurrentUser();
const { getUser, updateUser, isLoading: updateLoading } = useUser();
const navigate = useNavigate(); const navigate = useNavigate();
// Local state for user data fetched directly via API
const [user, setUser] = useState<User | null>(null);
const [userLoading, setUserLoading] = useState(false);
const [userError, setUserError] = useState<string | null>(null);
// Form state for user info
const [userForm, setUserForm] = useState({
username: '',
fullName: '',
email: '',
language: 'en' as Language,
privilege: '',
enabled: true
});
const [updateMessage, setUpdateMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const [isUpdating, setIsUpdating] = useState(false); // Flag to prevent form reset during update
const hasLoadedUser = useRef(false);
// Speech integration state // Speech integration state
const [hasSpeechIntegration, setHasSpeechIntegration] = useState(false); const [hasSpeechIntegration, setHasSpeechIntegration] = useState(false);
// Fetch user data directly using the /api/users/{userId} endpoint
const fetchUserData = async () => {
if (!currentUser?.id || hasLoadedUser.current) return;
hasLoadedUser.current = true;
setUserLoading(true);
setUserError(null);
try {
const userData = await getUser(currentUser.id);
setUser(userData);
} catch (error) {
console.error('Failed to fetch user data:', error);
setUserError(typeof error === 'string' ? error : 'Failed to load user data');
hasLoadedUser.current = false; // Reset on error to allow retry
} finally {
setUserLoading(false);
}
};
// Check for speech integration data // Check for speech integration data
const checkSpeechIntegration = () => { const checkSpeechIntegration = () => {
try { try {
@ -94,27 +54,6 @@ function Einstellungen() {
checkSpeechIntegration(); checkSpeechIntegration();
}, []); }, []);
// Fetch user data when currentUser is available
useEffect(() => {
if (currentUser?.id && !hasLoadedUser.current) {
fetchUserData();
}
}, [currentUser?.id]);
// Update form when user data is loaded (but not during an active update)
useEffect(() => {
if (user && !isUpdating) {
console.log('🔄 Updating form with user data:', user);
setUserForm({
username: user.username || '',
fullName: user.fullName || '',
email: user.email || '',
language: (user.language as Language) || currentLanguage,
privilege: user.privilege || '',
enabled: user.enabled
});
}
}, [user, currentLanguage, isUpdating]);
// Listen for speech integration updates // Listen for speech integration updates
useEffect(() => { useEffect(() => {
@ -237,110 +176,13 @@ function Einstellungen() {
localStorage.setItem('theme', newIsDarkMode ? 'dark' : 'light'); localStorage.setItem('theme', newIsDarkMode ? 'dark' : 'light');
}; };
const handleUserFormChange = (field: keyof typeof userForm, value: string | boolean) => {
setUserForm(prev => ({ ...prev, [field]: value }));
setUpdateMessage(null); // Clear any previous messages
// Language change will be handled when the form is submitted, not immediately if (isLoading) {
};
const handleSaveUserInfo = async () => {
if (!user) return;
setIsUpdating(true); // Prevent form reset during update
try {
// Create complete User object with updated form data
// Only include editable fields based on authentication authority
const completeUserData: User = {
id: user.id,
username: user.authenticationAuthority === 'local' ? userForm.username : user.username,
fullName: user.authenticationAuthority === 'local' ? userForm.fullName : user.fullName,
email: user.authenticationAuthority === 'local' ? userForm.email : user.email,
language: userForm.language, // Language is always editable
privilege: userForm.privilege,
enabled: userForm.enabled,
authenticationAuthority: user.authenticationAuthority,
mandateId: user.mandateId
};
// Update user via API - this returns the updated user
const updatedUser = await updateUser(user.id, completeUserData);
if (updatedUser) {
console.log('✅ User update successful:', updatedUser);
// Update local user state with the returned data
setUser(updatedUser);
// Update frontend language if it was changed
const newLanguage = updatedUser.language as Language;
if (newLanguage && newLanguage !== currentLanguage) {
try {
await setLanguage(newLanguage);
console.log('🌍 Frontend language updated to:', newLanguage);
} catch (error) {
console.error('Failed to change frontend language:', error);
}
}
// Success: Update form with the actual returned data to ensure consistency
setUserForm({
username: updatedUser.username || '',
fullName: updatedUser.fullName || '',
email: updatedUser.email || '',
language: newLanguage || currentLanguage,
privilege: updatedUser.privilege || '',
enabled: updatedUser.enabled
});
console.log('📝 Form updated with new data');
// Dispatch event to notify other components (like sidebar) that user data was updated
window.dispatchEvent(new CustomEvent('userInfoUpdated'));
setUpdateMessage({ type: 'success', text: t('settings.userinfo.success') });
} else {
throw new Error('No updated user data returned from server');
}
// Clear message after 3 seconds
setTimeout(() => setUpdateMessage(null), 3000);
} catch (error) {
console.error('Failed to update user info:', error);
setUpdateMessage({ type: 'error', text: t('settings.userinfo.update_error') });
// Reset form to original user data on error
if (user) {
setUserForm({
username: user.username || '',
fullName: user.fullName || '',
email: user.email || '',
language: (user.language as Language) || currentLanguage,
privilege: user.privilege || '',
enabled: user.enabled
});
}
} finally {
setIsUpdating(false); // Re-enable form sync
}
};
const getLanguageLabel = (lang: Language): string => {
switch (lang) {
case 'de': return t('language.german');
case 'en': return t('language.english');
case 'fr': return t('language.french');
default: return lang;
}
};
if (isLoading || currentUserLoading || userLoading) {
return ( return (
<div className={sharedStyles.pageContainer}> <div className={sharedStyles.pageContainer}>
<div className={sharedStyles.pageCard}> <div className={sharedStyles.pageCard}>
<div style={{ padding: '2rem', textAlign: 'center' }}> <div style={{ padding: '2rem', textAlign: 'center' }}>
{isLoading ? t('common.loading') : t('settings.userinfo.loading')} {t('common.loading')}
</div> </div>
</div> </div>
</div> </div>
@ -355,143 +197,7 @@ function Einstellungen() {
<div className={sharedStyles.contentArea}> <div className={sharedStyles.contentArea}>
{/* User Information Section */} {/* User Information Section */}
<SettingsUser />
{userError && (
<div className={styles.errorMessage}>
{t('settings.userinfo.error')}: {typeof userError === 'string' ? userError : 'An error occurred'}
</div>
)}
{user && (
<div className={styles.userInfoForm}>
<span className={styles.settingLabel}>{t('settings.userinfo')}</span>
<span className={styles.settingDescription}>
{t('settings.userinfo.description')}
</span>
<div className={styles.formRow}>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.userinfo.username')}
{user.authenticationAuthority !== 'local' && (
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
)}
</label>
<input
type="text"
className={styles.formInput}
value={userForm.username}
onChange={(e) => handleUserFormChange('username', e.target.value)}
placeholder={t('settings.userinfo.username')}
readOnly={user.authenticationAuthority !== 'local'}
title={user.authenticationAuthority !== 'local' ?
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
undefined}
/>
</div>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.userinfo.fullname')}
{user.authenticationAuthority !== 'local' && (
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
)}
</label>
<input
type="text"
className={styles.formInput}
value={userForm.fullName}
onChange={(e) => handleUserFormChange('fullName', e.target.value)}
placeholder={t('settings.userinfo.fullname')}
readOnly={user.authenticationAuthority !== 'local'}
title={user.authenticationAuthority !== 'local' ?
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
undefined}
/>
</div>
</div>
<div className={styles.formRow}>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.userinfo.email')}
{user.authenticationAuthority !== 'local' && (
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
)}
</label>
<input
type="email"
className={styles.formInput}
value={userForm.email}
onChange={(e) => handleUserFormChange('email', e.target.value)}
placeholder={t('settings.userinfo.email')}
readOnly={user.authenticationAuthority !== 'local'}
title={user.authenticationAuthority !== 'local' ?
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
undefined}
/>
</div>
<div className={styles.formField}>
<label className={styles.fieldLabel}>
{t('settings.language')}
<span className={styles.fieldNote}>({t('settings.language.description')})</span>
</label>
<select
className={styles.formSelect}
value={userForm.language}
onChange={(e) => handleUserFormChange('language', e.target.value)}
aria-label={t('settings.language')}
>
<option value="de">{getLanguageLabel('de')}</option>
<option value="en">{getLanguageLabel('en')}</option>
<option value="fr">{getLanguageLabel('fr')}</option>
</select>
</div>
</div>
<div className={styles.formRow}>
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('settings.userinfo.privilege')}</label>
<input
type="text"
className={styles.formInput}
value={userForm.privilege}
readOnly
placeholder={t('settings.userinfo.privilege')}
title="This field is read-only"
/>
</div>
<div className={styles.formField}>
<label className={styles.fieldLabel}>{t('settings.userinfo.auth_authority')}</label>
<input
type="text"
className={styles.formInput}
value={user.authenticationAuthority}
readOnly
placeholder={t('settings.userinfo.auth_authority')}
title="This field is read-only"
/>
</div>
</div>
{updateMessage && (
<div className={`${styles.updateMessage} ${updateMessage.type === 'success' ? styles.successMessage : styles.errorMessage}`}>
{updateMessage.text}
</div>
)}
<div className={styles.formActions}>
<button
className={styles.saveButton}
onClick={handleSaveUserInfo}
disabled={updateLoading}
>
{updateLoading ? t('settings.userinfo.saving') : t('settings.userinfo.save')}
</button>
</div>
</div>
)}
{/* Theme Setting */} {/* Theme Setting */}
<div className={styles.settingItem}> <div className={styles.settingItem}>

View file

@ -1,16 +1,38 @@
import { useState } from 'react';
import { IoMdAdd } from 'react-icons/io';
import { useLanguage } from '../../contexts/LanguageContext';
import sharedStyles from '../../components/PageManager/pages.module.css'; import sharedStyles from '../../components/PageManager/pages.module.css';
import MitgliederTable from '../../components/Mitglieder/MitgliederTable'; import MitgliederTable from '../../components/Mitglieder/MitgliederTable';
function TeamBereich () { function TeamBereich() {
const { t } = useLanguage();
const [showAddUser, setShowAddUser] = useState(false);
const handleAddUser = () => {
setShowAddUser(true);
};
return ( return (
<div className={sharedStyles.pageContainer}> <div className={sharedStyles.pageContainer}>
<div className={sharedStyles.pageCard}> <div className={sharedStyles.pageCard}>
<div className={sharedStyles.pageHeader}> <div className={sharedStyles.pageHeader}>
<h1 className={sharedStyles.pageTitle}>Team-Bereich</h1> <h1 className={sharedStyles.pageTitle}>{t('nav.team', 'Team-Bereich')}</h1>
</div> <button
<div className={sharedStyles.pageContent}> className={sharedStyles.primaryButton}
<MitgliederTable /> onClick={handleAddUser}
aria-label={t('users.add.title', 'Add User')}
>
<span className={sharedStyles.buttonIcon}><IoMdAdd /></span>
{t('users.add.button', 'Add User')}
</button>
</div>
<div className={sharedStyles.horizontalDivider}></div>
<div className={sharedStyles.contentArea}>
<MitgliederTable
showAddUser={showAddUser}
onAddUserClose={() => setShowAddUser(false)}
/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -2,7 +2,7 @@ import { useNavigate, useLocation } from 'react-router-dom';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { FaGoogle, FaMicrosoft } from 'react-icons/fa'; import { FaGoogle, FaMicrosoft } from 'react-icons/fa';
import { useAuth, useMsalAuth } from '../hooks/useAuthentication'; import { useAuth, useMsalAuth, useGoogleAuth } from '../hooks/useAuthentication';
import styles from './Login.module.css'; import styles from './Login.module.css';
@ -16,6 +16,7 @@ function Login() {
const [passwordFocused, setPasswordFocused] = useState(false); const [passwordFocused, setPasswordFocused] = useState(false);
const { login, error: loginError, isLoading: isLoginLoading } = useAuth(); const { login, error: loginError, isLoading: isLoginLoading } = useAuth();
const { loginWithMsal, error: msalError, isLoading: isMsalLoading } = useMsalAuth(); const { loginWithMsal, error: msalError, isLoading: isMsalLoading } = useMsalAuth();
const { loginWithGoogle, error: googleError, isLoading: isGoogleLoading } = useGoogleAuth();
// Get the page the user was trying to visit // Get the page the user was trying to visit
const from = location.state?.from?.pathname || "/"; const from = location.state?.from?.pathname || "/";
@ -57,6 +58,17 @@ function Login() {
} }
}; };
const handleGoogleLogin = async () => {
try {
console.log("Attempting Google login...");
const response = await loginWithGoogle();
console.log("Google login successful:", response);
navigate(from, { replace: true });
} catch (error) {
console.error("Google login failed:", error);
}
};
const handleCredentialLogin = async (e?: React.MouseEvent) => { const handleCredentialLogin = async (e?: React.MouseEvent) => {
e?.preventDefault(); // Prevent default form submission e?.preventDefault(); // Prevent default form submission
try { try {
@ -84,8 +96,8 @@ function Login() {
<div className={styles.loginSection}> <div className={styles.loginSection}>
<div className={styles.loginBox}> <div className={styles.loginBox}>
<div className={styles.loginForm}> <div className={styles.loginForm}>
{(loginError || msalError) && ( {(loginError || msalError || googleError) && (
<div className={styles.error}>{loginError || msalError}</div> <div className={styles.error}>{loginError || msalError || googleError}</div>
)} )}
<div className={styles.floatingLabelInput}> <div className={styles.floatingLabelInput}>
<input <input
@ -153,12 +165,12 @@ function Login() {
<button <button
className={`${styles.button} ${styles.googleButton}`} className={`${styles.button} ${styles.googleButton}`}
onClick={() => console.log("Google button clicked")} onClick={handleGoogleLogin}
disabled={false} disabled={isGoogleLoading}
> >
<div className={styles.buttonContent}> <div className={styles.buttonContent}>
<FaGoogle /> <FaGoogle />
Mit Google anmelden {isGoogleLoading ? "Signing in..." : "Mit Google anmelden"}
</div> </div>
</button> </button>