diff --git a/src/components/Connections/ConnectionsTable.module.css b/src/components/Connections/ConnectionsTable.module.css index f2a3931..22bd13b 100644 --- a/src/components/Connections/ConnectionsTable.module.css +++ b/src/components/Connections/ConnectionsTable.module.css @@ -32,6 +32,17 @@ 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 */ @media (max-width: 768px) { .tableContainer { diff --git a/src/components/Connections/connectionsInterfaces.ts b/src/components/Connections/connectionsInterfaces.ts index 1dc3da1..7d5c11b 100644 --- a/src/components/Connections/connectionsInterfaces.ts +++ b/src/components/Connections/connectionsInterfaces.ts @@ -38,7 +38,7 @@ export interface ConnectionsErrorDisplayProps { // Table Action Interface export interface TableAction { label: string; - onClick: (connection: Connection) => Promise | void; + onClick?: (connection: Connection) => Promise | void; icon: React.ReactNode | ((connection: Connection) => React.ReactNode); } @@ -53,7 +53,7 @@ export interface ConnectionHandlers { handleDisconnect: (connection: Connection) => Promise; handleDelete: (connection: Connection) => Promise; handleDeleteMultiple: (connections: Connection[]) => Promise; - handleConnectOrDisconnect: (connection: Connection) => Promise; + handleUpdateConnection: (connection: Connection) => Promise; handleEditConnection: (connection: Connection) => Promise; handleSaveConnection: (updatedConnection: Connection) => Promise; handleCancelEdit: () => void; diff --git a/src/components/Connections/connectionsLogic.tsx b/src/components/Connections/connectionsLogic.tsx index aaecf24..3f8ddda 100644 --- a/src/components/Connections/connectionsLogic.tsx +++ b/src/components/Connections/connectionsLogic.tsx @@ -1,7 +1,7 @@ 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 { GoUnlink } from 'react-icons/go'; + import { useConnections, useOAuthConnect, useDisconnect } from '../../hooks/useConnections'; @@ -24,7 +24,9 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { fetchConnections, createConnection, updateConnection, - deleteConnection, + deleteConnection, + refreshMicrosoftToken, + refreshGoogleToken, isLoading, error } = useConnections(); @@ -167,11 +169,15 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { type: 'enum', filterOptions: ['active', 'pending', 'expired', 'revoked'], 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, sortable: true, - filterable: true + filterable: true, + cellClassName: (value: string) => { + return value === 'expired' ? 'expired-connection' : ''; + } }, { key: 'externalUsername', @@ -282,9 +288,29 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { } ]; - // Fetch connections on mount + // Fetch connections on mount and auto-connect them 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 @@ -362,11 +388,23 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { } }; - const handleConnectOrDisconnect = async (connection: Connection) => { - if (connection.status === 'active') { - await handleDisconnect(connection); - } else { - await handleConnect(connection); + const handleUpdateConnection = async (connection: Connection) => { + console.log('Updating connection:', connection); + try { + if (connection.status === 'expired') { + // 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: }, { - label: t('connections.action.toggle_connection', 'Toggle Connection'), - onClick: handleConnectOrDisconnect, - icon: (connection: Connection) => connection.status === 'active' ? : + label: t('connections.action.update', 'Update'), + onClick: handleUpdateConnection, + icon: }, { label: t('connections.action.delete', 'Delete'), - onClick: handleDelete, - icon: + icon: , + // onClick is handled by FormGenerator for delete confirmation } ]; @@ -440,7 +478,7 @@ export function useConnectionsLogic(): ConnectionsLogicReturn { handleDisconnect, handleDelete, handleDeleteMultiple, - handleConnectOrDisconnect, + handleUpdateConnection, handleEditConnection, handleSaveConnection, handleCancelEdit diff --git a/src/components/Dateien/dateienInterfaces.ts b/src/components/Dateien/dateienInterfaces.ts index 8a32a2e..1b958fb 100644 --- a/src/components/Dateien/dateienInterfaces.ts +++ b/src/components/Dateien/dateienInterfaces.ts @@ -16,7 +16,7 @@ export interface DateienTableProps { // Table Action Interface export interface TableAction { label: string; - onClick: (file: UserFile) => Promise | void; + onClick?: (file: UserFile) => Promise | void; icon: React.ReactNode | ((file: UserFile) => React.ReactNode); } diff --git a/src/components/Dateien/dateienLogic.tsx b/src/components/Dateien/dateienLogic.tsx index 789b70e..ab8193c 100644 --- a/src/components/Dateien/dateienLogic.tsx +++ b/src/components/Dateien/dateienLogic.tsx @@ -308,48 +308,43 @@ export function useDateienLogic(): DateienLogicReturn { // Handle file deletion const handleDelete = async (file: UserFile) => { - if (window.confirm(t('files.delete.confirm').replace('{name}', file.file_name))) { - // Immediately remove from UI for instant feedback - removeFileOptimistically(file.id); - - const success = await handleFileDelete(file.id); - - if (!success && deleteError) { - console.error('Delete failed:', deleteError); - // Refetch to restore the file in case of failure - refetch(); - } + // Immediately remove from UI for instant feedback + removeFileOptimistically(file.id); + + const success = await handleFileDelete(file.id); + + if (!success && deleteError) { + console.error('Delete failed:', deleteError); + // Refetch to restore the file in case of failure + refetch(); } }; // Handle multiple file deletion const handleDeleteMultiple = async (filesToDelete: UserFile[]) => { - const fileCount = filesToDelete.length; - if (window.confirm(t('files.delete.confirmMultiple', 'Are you sure you want to delete {count} files?').replace('{count}', fileCount.toString()))) { - // Immediately remove all files from UI for instant feedback - filesToDelete.forEach(file => removeFileOptimistically(file.id)); - - // Start all delete operations simultaneously - const deletePromises = filesToDelete.map(async (file) => { - try { - const success = await handleFileDelete(file.id); - return { fileId: file.id, success }; - } catch (error) { - console.error('Failed to delete file:', file.id, error); - 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(); + // Immediately remove all files from UI for instant feedback + filesToDelete.forEach(file => removeFileOptimistically(file.id)); + + // Start all delete operations simultaneously + const deletePromises = filesToDelete.map(async (file) => { + try { + const success = await handleFileDelete(file.id); + return { fileId: file.id, success }; + } catch (error) { + console.error('Failed to delete file:', file.id, error); + 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(); } }; @@ -390,18 +385,10 @@ export function useDateienLogic(): DateienLogicReturn { }, { label: t('files.action.delete'), - icon: (row: UserFile) => { - const isDeletingThis = deletingFiles.has(row.id); - if (isDeletingThis) return '⏳'; - return ; - }, - onClick: (row: UserFile) => { - if (!deletingFiles.has(row.id)) { - handleDelete(row); - } - } + icon: , + // onClick is handled by FormGenerator for delete confirmation } - ], [t, previewingFiles, downloadingFiles, deletingFiles, handleDownload, handleDelete]); + ], [t, previewingFiles, downloadingFiles, handleDownload, handleDelete]); return { files, diff --git a/src/components/FormGenerator/FormGenerator.module.css b/src/components/FormGenerator/FormGenerator.module.css index 2e138c5..5e63708 100644 --- a/src/components/FormGenerator/FormGenerator.module.css +++ b/src/components/FormGenerator/FormGenerator.module.css @@ -334,6 +334,26 @@ 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 { position: sticky; top: 0; @@ -401,7 +421,7 @@ } .tr:hover { - background: var(--color-gray-disabled); + background: transparent; } .tr.selected { @@ -514,7 +534,42 @@ tbody .actionsColumn { 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 { display: flex; justify-content: space-between; @@ -684,7 +739,7 @@ tbody .actionsColumn { } .tr:hover { - background: rgba(255, 255, 255, 0.05); + background: transparent; } .tr.selected { diff --git a/src/components/FormGenerator/FormGenerator.tsx b/src/components/FormGenerator/FormGenerator.tsx index daf9d40..cefbf6e 100644 --- a/src/components/FormGenerator/FormGenerator.tsx +++ b/src/components/FormGenerator/FormGenerator.tsx @@ -2,7 +2,7 @@ import React, { useState, useMemo, useRef, useEffect } from 'react'; import { useLanguage } from '../../contexts/LanguageContext'; import styles from './FormGenerator.module.css'; -import { IoIosRefresh } from "react-icons/io"; +import { IoIosRefresh, IoIosCheckmark, IoIosClose } from "react-icons/io"; // Types for the FormGenerator export interface ColumnConfig { @@ -17,6 +17,7 @@ export interface ColumnConfig { searchable?: boolean; formatter?: (value: any, row: any) => React.ReactNode; filterOptions?: string[]; // For enum/select filters + cellClassName?: (value: any, row: any) => string; // For custom cell styling } export interface FormGeneratorProps { @@ -45,6 +46,7 @@ export interface FormGeneratorProps { onDeleteMultiple?: (rows: T[]) => void; onRefresh?: () => void; className?: string; + getRowDataAttributes?: (row: T, index: number) => Record; } export function FormGenerator>({ @@ -67,7 +69,8 @@ export function FormGenerator>({ onDelete, onDeleteMultiple, onRefresh, - className = '' + className = '', + getRowDataAttributes }: FormGeneratorProps) { const { t } = useLanguage(); // Auto-detect columns if not provided @@ -115,6 +118,13 @@ export function FormGenerator>({ const [currentPage, setCurrentPage] = useState(1); const [currentPageSize, setCurrentPageSize] = useState(pageSize); + // Delete confirmation state + const [deleteConfirmRow, setDeleteConfirmRow] = useState(null); + const [deletingRows, setDeletingRows] = useState>(new Set()); + + // Refs for action buttons containers to detect clicks outside + const actionButtonsRefs = useRef>(new Map()); + // Refs for resizing const tableRef = useRef(null); const resizingColumn = useRef(null); @@ -132,6 +142,28 @@ export function FormGenerator>({ setColumnWidths(initialWidths); }, [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 const filteredData = useMemo(() => { let result = [...data]; @@ -293,6 +325,43 @@ export function FormGenerator>({ } }; + // 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 const handleDeleteMultiple = () => { if (onDeleteMultiple && selectedRows.size > 0) { @@ -680,12 +749,17 @@ export function FormGenerator>({ - {paginatedData.map((row, index) => ( - onRowClick?.(row, index)} - > + {paginatedData.map((row, index) => { + const dataAttributes = getRowDataAttributes ? getRowDataAttributes(row, index) : {}; + return ( + onRowClick?.(row, index)} + {...Object.fromEntries( + Object.entries(dataAttributes).map(([key, value]) => [`data-${key}`, value]) + )} + > {selectable && ( >({ className={styles.actionsColumn} style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }} > -
+
{ + if (el) { + actionButtonsRefs.current.set(index, el); + } else { + actionButtonsRefs.current.delete(index); + } + }} + className={styles.actionButtons} + > {actions.map((action, actionIndex) => { 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 ( +
+ + +
+ ); + } + return (
)} - {detectedColumns.map(column => ( - - {formatCellValue(row[column.key], column, row)} - - ))} + {detectedColumns.map(column => { + const cellValue = row[column.key]; + const customClassName = column.cellClassName ? column.cellClassName(cellValue, row) : ''; + const combinedClassName = `${styles.td} ${customClassName}`.trim(); + + return ( + + {formatCellValue(cellValue, column, row)} + + ); + })} - ))} + ); + })} )} diff --git a/src/components/Mitglieder/MitgliederTable.module.css b/src/components/Mitglieder/MitgliederTable.module.css index cf676b0..f1b7e60 100644 --- a/src/components/Mitglieder/MitgliederTable.module.css +++ b/src/components/Mitglieder/MitgliederTable.module.css @@ -7,6 +7,47 @@ 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 */ .mitgliederFormGenerator { flex: 1; @@ -61,4 +102,62 @@ color: var(--color-text); 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; +} + \ No newline at end of file diff --git a/src/components/Mitglieder/MitgliederTable.tsx b/src/components/Mitglieder/MitgliederTable.tsx index fde97f9..6cdd1ca 100644 --- a/src/components/Mitglieder/MitgliederTable.tsx +++ b/src/components/Mitglieder/MitgliederTable.tsx @@ -1,10 +1,12 @@ import { FormGenerator } from '../FormGenerator/FormGenerator'; +import { Popup } from '../Popup/Popup'; +import { EditForm, EditFieldConfig } from '../Popup/EditForm'; import { useMitgliederLogic } from './mitgliederLogic'; import { MitgliederTableProps } from './mitgliederTypes'; import { useLanguage } from '../../contexts/LanguageContext'; import styles from './MitgliederTable.module.css'; -function MitgliederTable({ className = '' }: MitgliederTableProps) { +function MitgliederTable({ className = '', showAddUser = false, onAddUserClose }: MitgliederTableProps) { const { t } = useLanguage(); const { users, @@ -12,9 +14,132 @@ function MitgliederTable({ className = '' }: MitgliederTableProps) { error, columns, actions, - refetch + refetch, + editingUser, + handleSaveUser, + handleCancelEdit, + deletingUser, + handleConfirmDelete, + handleCancelDelete, + handleSaveNewUser } = 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) { return (
@@ -43,7 +168,95 @@ function MitgliederTable({ className = '' }: MitgliederTableProps) { actions={actions} onRefresh={refetch} 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 && ( + + + + )} + + {/* Delete Confirmation Popup */} + {deletingUser && ( + +
+

{t('users.delete.message', 'Are you sure you want to delete this user?')}

+
+ {t('users.column.name', 'Name')}: {deletingUser.fullName || deletingUser.username} +
+
+ {t('users.column.email', 'Email')}: {deletingUser.email} +
+

+ {t('users.delete.warning', 'This action cannot be undone.')} +

+
+
+ )} + + {/* Add User Popup */} + {showAddUser && ( + + + + )}
); } diff --git a/src/components/Mitglieder/mitgliederLogic.tsx b/src/components/Mitglieder/mitgliederLogic.tsx index da1965d..c57c337 100644 --- a/src/components/Mitglieder/mitgliederLogic.tsx +++ b/src/components/Mitglieder/mitgliederLogic.tsx @@ -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 { useLanguage } from '../../contexts/LanguageContext'; @@ -10,8 +12,10 @@ import type { } from './mitgliederTypes'; export function useMitgliederLogic(): MitgliederLogicReturn { - const { users, loading, error, refetch } = useOrgUsers(); + const { users, loading, error, refetch, updateUser, deleteUser, createUser } = useOrgUsers(); const { t } = useLanguage(); + const [editingUser, setEditingUser] = useState(null); + const [deletingUser, setDeletingUser] = useState(null); // Configure columns for the users table const columns: UserColumnConfig[] = useMemo(() => [ @@ -85,11 +89,157 @@ export function useMitgliederLogic(): MitgliederLogicReturn { ); } + }, + { + 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 = { + '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 ( + + {value ? privilegeMap[value] || value : t('users.noPrivilege', 'No Privilege')} + + ); + } + }, + { + key: 'enabled', + label: t('users.column.enabled', 'Enabled'), + type: 'boolean', + width: 100, + minWidth: 80, + maxWidth: 120, + sortable: true, + filterable: true, + formatter: (value: boolean | undefined) => ( + + {value ? t('users.enabled.yes', 'Yes') : t('users.enabled.no', 'No')} + + ) + }, + { + 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 = { + 'local': t('users.auth.local', 'Local'), + 'msft': t('users.auth.msft', 'Microsoft') + }; + return ( + + {value ? authMap[value] || value : t('users.noAuthAuthority', 'No Auth Authority')} + + ); + } } ], [t]); - // Configure action buttons (empty for now) - const actions: UserActionConfig[] = useMemo(() => [], []); + // Handle edit user + 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: () => , + onClick: (row: any) => handleEditUser(row) + }, + { + label: t('users.actions.delete', 'Delete'), + icon: () => , + onClick: (row: any) => handleDeleteUser(row) + } + ], [t]); return { // Data @@ -102,6 +252,22 @@ export function useMitgliederLogic(): MitgliederLogicReturn { // Additional data for rendering columns, - actions + actions, + + // Edit functionality + editingUser, + setEditingUser, + handleSaveUser, + handleCancelEdit, + + // Delete functionality + deletingUser, + setDeletingUser, + handleConfirmDelete, + handleCancelDelete, + + // Add user functionality + handleSaveNewUser, + handleCancelAddUser }; } diff --git a/src/components/Mitglieder/mitgliederTypes.ts b/src/components/Mitglieder/mitgliederTypes.ts index 4e8c4f2..7c1a5db 100644 --- a/src/components/Mitglieder/mitgliederTypes.ts +++ b/src/components/Mitglieder/mitgliederTypes.ts @@ -4,6 +4,8 @@ import { User } from '../../hooks/useUsers'; // Props for the MitgliederTable component export interface MitgliederTableProps { className?: string; + showAddUser?: boolean; + onAddUserClose?: () => void; } // Action configuration for user actions @@ -41,4 +43,20 @@ export interface MitgliederLogicReturn { // Additional data for rendering columns: UserColumnConfig[]; actions: UserActionConfig[]; + + // Edit functionality + editingUser: User | null; + setEditingUser: (user: User | null) => void; + handleSaveUser: (updatedUser: User) => Promise; + handleCancelEdit: () => void; + + // Delete functionality + deletingUser: User | null; + setDeletingUser: (user: User | null) => void; + handleConfirmDelete: () => Promise; + handleCancelDelete: () => void; + + // Add user functionality + handleSaveNewUser: (userData: any) => Promise; + handleCancelAddUser: () => void; } diff --git a/src/components/PageManager/PRIVILEGE_IMPLEMENTATION.md b/src/components/PageManager/PRIVILEGE_IMPLEMENTATION.md new file mode 100644 index 0000000..e41289f --- /dev/null +++ b/src/components/PageManager/PRIVILEGE_IMPLEMENTATION.md @@ -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 diff --git a/src/components/PageManager/README.md b/src/components/PageManager/README.md new file mode 100644 index 0000000..e9c8c5c --- /dev/null +++ b/src/components/PageManager/README.md @@ -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; +``` + +### 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 diff --git a/src/components/PageManager/pageConfigInterface.ts b/src/components/PageManager/pageConfigInterface.ts index aeccaf7..3c5cbb9 100644 --- a/src/components/PageManager/pageConfigInterface.ts +++ b/src/components/PageManager/pageConfigInterface.ts @@ -1,5 +1,9 @@ import React from 'react'; import { IconType } from 'react-icons'; +import { SidebarSubmenuItemData } from '../Sidebar/sidebarTypes'; + +// Generic privilege checker function type +export type PrivilegeChecker = () => boolean | Promise; // Extended page configuration interface that includes sidebar properties export interface PageConfig { @@ -22,7 +26,15 @@ export interface PageConfig { order?: number; // For sidebar ordering 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[]; // Lifecycle hooks diff --git a/src/components/PageManager/pageConfigs.ts b/src/components/PageManager/pageConfigs.ts index 2a57368..b37cdaf 100644 --- a/src/components/PageManager/pageConfigs.ts +++ b/src/components/PageManager/pageConfigs.ts @@ -1,4 +1,5 @@ import { PageConfig, SidebarItem } from './pageConfigInterface'; +import { privilegeCheckers } from '../../hooks/privilegeCheckers'; import { lazy } from 'react'; // Import icons for sidebar @@ -35,6 +36,8 @@ export const pageConfigs: PageConfig[] = [ icon: LuTicket, order: 1, showInSidebar: true, + // All privilege levels can access dashboard + privilegeChecker: privilegeCheckers.viewerRole, onActivate: async () => { if (import.meta.env.DEV) console.log('Dashboard activated - state preserved'); // You can add analytics tracking here @@ -55,6 +58,8 @@ export const pageConfigs: PageConfig[] = [ icon: FaRegFileAlt, order: 2, showInSidebar: true, + // All privilege levels can access dateien + privilegeChecker: privilegeCheckers.viewerRole, onActivate: async () => { if (import.meta.env.DEV) console.log('Dateien activated'); }, @@ -77,6 +82,8 @@ export const pageConfigs: PageConfig[] = [ icon: LuMessageSquareText , order: 3, showInSidebar: true, + // All privilege levels can access prompts + privilegeChecker: privilegeCheckers.viewerRole, onActivate: async () => { if (import.meta.env.DEV) console.log('Prompts activated'); }, @@ -99,6 +106,8 @@ export const pageConfigs: PageConfig[] = [ icon: MdOutlineWorkOutline, order: 5, showInSidebar: true, + // Privilege checking - only admin and sysadmin can access team management + privilegeChecker: privilegeCheckers.adminRole, onActivate: async () => { if (import.meta.env.DEV) console.log('Team Bereich activated'); } @@ -115,6 +124,8 @@ export const pageConfigs: PageConfig[] = [ icon: FaPlug, order: 4, showInSidebar: true, + // All privilege levels can access connections + privilegeChecker: privilegeCheckers.viewerRole, onActivate: async () => { if (import.meta.env.DEV) console.log('Connections activated'); }, @@ -134,6 +145,8 @@ export const pageConfigs: PageConfig[] = [ icon: LuWorkflow, order: 3, showInSidebar: true, + // All privilege levels can access workflows + privilegeChecker: privilegeCheckers.viewerRole, onActivate: async () => { if (import.meta.env.DEV) console.log('Workflows activated - preserving workflow state'); }, @@ -159,6 +172,8 @@ export const pageConfigs: PageConfig[] = [ icon: GoGear, order: 6, showInSidebar: true, + // All privilege levels can access settings + privilegeChecker: privilegeCheckers.viewerRole, onActivate: async () => { if (import.meta.env.DEV) console.log('Einstellungen activated'); } @@ -175,6 +190,12 @@ export const pageConfigs: PageConfig[] = [ icon: FaRegFileAlt, order: 7, showInSidebar: true, + // All privilege levels can access speech + privilegeChecker: privilegeCheckers.viewerRole, + // Generic subpage support + hasSubpages: true, + subpagePrefix: 'speech', + subpagePrivilegeChecker: privilegeCheckers.speechSignup, onActivate: async () => { if (import.meta.env.DEV) console.log('Speech activated'); } @@ -191,6 +212,8 @@ export const pageConfigs: PageConfig[] = [ icon: IoIosDocument, order: 8, showInSidebar: false, // Will be shown as subpage under Speech + // Privilege checking - only users with speech signup can access transcripts + privilegeChecker: privilegeCheckers.speechSignup, onActivate: async () => { 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 -export const getSidebarItems = () => { +export const getSidebarItems = async () => { const items: SidebarItem[] = []; - pageConfigs + // Process each page config + for (const config of pageConfigs .filter(config => config.showInSidebar !== false) - .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(); + .sort((a, b) => (a.order || 0) - (b.order || 0))) { + + // Check if user has privilege to access this page + let hasPagePrivilege = true; + 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; + } + } + + // Skip this page if user doesn't have privilege + 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 (hasSignedUp) { - // Find the transcript subpage - const transcriptConfig = pageConfigs.find(c => c.path === 'speech/transcripts'); + 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 (transcriptConfig) { - // Create expandable Speech item with submenu + if (subpages.length > 0) { + // Create expandable item with submenu items.push({ id: config.id, name: config.name, @@ -254,16 +301,14 @@ export const getSidebarItems = () => { icon: config.icon, moduleEnabled: config.moduleEnabled ?? true, order: config.order || 0, - submenu: [ - { - id: transcriptConfig.id, - name: transcriptConfig.name, - link: `/${transcriptConfig.path}` - } - ] + submenu: subpages.map(subpage => ({ + id: subpage.id, + name: subpage.name, + link: `/${subpage.path}` + })) }); } else { - // Fallback to regular item if transcript config not found + // No subpages found, show as regular item items.push({ id: config.id, name: config.name, @@ -274,7 +319,7 @@ export const getSidebarItems = () => { }); } } else { - // User hasn't signed up, show regular non-expandable item + // No subpage privilege, show as regular non-expandable item items.push({ id: config.id, name: config.name, @@ -284,8 +329,9 @@ export const getSidebarItems = () => { order: config.order || 0 }); } - } else { - // Regular non-Speech items + } catch (error) { + console.error(`Error checking subpage privilege for ${config.path}:`, error); + // Fallback to regular item on error items.push({ id: config.id, name: config.name, @@ -295,40 +341,21 @@ export const getSidebarItems = () => { 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; }; -// 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; diff --git a/src/components/PageManager/subpageExamples.ts b/src/components/PageManager/subpageExamples.ts new file mode 100644 index 0000000..8c0f51e --- /dev/null +++ b/src/components/PageManager/subpageExamples.ts @@ -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 +*/ diff --git a/src/components/Prompts/promptsLogic.tsx b/src/components/Prompts/promptsLogic.tsx index a7c0229..a61fe06 100644 --- a/src/components/Prompts/promptsLogic.tsx +++ b/src/components/Prompts/promptsLogic.tsx @@ -295,12 +295,7 @@ export function usePromptsLogic(): PromptsLogicReturn { /> ); }, - onClick: (row: Prompt) => { - const isDeletable = isPromptDeletable(row); - if (isDeletable && !deletingPrompts.has(row.id)) { - handleDeletePrompt(row); - } - } + // onClick is handled by FormGenerator for delete confirmation }, ], [t, deletingPrompts, handleDeletePrompt, handleEditPrompt, handleCopyPrompt]); diff --git a/src/components/Prompts/promptsTypes.ts b/src/components/Prompts/promptsTypes.ts index fbc1495..a8db13e 100644 --- a/src/components/Prompts/promptsTypes.ts +++ b/src/components/Prompts/promptsTypes.ts @@ -19,7 +19,7 @@ export interface PromptsTableProps { export interface PromptActionConfig { label: string | ((row: Prompt) => string); icon: (row: Prompt) => React.ReactElement; - onClick: (row: Prompt) => void; + onClick?: (row: Prompt) => void; } // Column configuration for the prompts table diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index e235d93..d86aa71 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -11,6 +11,9 @@ import { GoSidebarExpand, GoSidebarCollapse } from 'react-icons/go'; const Sidebar: React.FC = ({ data }) => { const sidebar = useSidebarLogic(); + // Ensure data is always an array + const sidebarItems = Array.isArray(data) ? data : []; + return (
= ({ data }) => {
- {data.map(item => { + {sidebarItems.map(item => { return ( = ({ data }) => { } const SidebarWithData: React.FC = () => { - const sidebarData = useSidebarFromPageConfigs(); + const { items: sidebarData, isLoading } = useSidebarFromPageConfigs(); + + if (isLoading) { + return ( +
+
+
+
+ Power + On +
+
+
+
+
+ Loading... +
+
+
+ ); + } + return ; }; diff --git a/src/components/TestSharepoint/testSharepointInterfaces.ts b/src/components/TestSharepoint/testSharepointInterfaces.ts index 122ef6e..515dff0 100644 --- a/src/components/TestSharepoint/testSharepointInterfaces.ts +++ b/src/components/TestSharepoint/testSharepointInterfaces.ts @@ -29,7 +29,7 @@ export interface TestSharepointTableProps { // Table Action Interface export interface TableAction { label: string; - onClick: (document: SharePointDocument) => Promise | void; + onClick?: (document: SharePointDocument) => Promise | void; icon: React.ReactNode | ((document: SharePointDocument) => React.ReactNode); } diff --git a/src/components/Workflows/workflowsLogic.tsx b/src/components/Workflows/workflowsLogic.tsx index ae7bfb9..c3c94db 100644 --- a/src/components/Workflows/workflowsLogic.tsx +++ b/src/components/Workflows/workflowsLogic.tsx @@ -417,12 +417,8 @@ export function useWorkflowsLogic(): WorkflowsLogicReturn { label: t('workflows.action.delete'), icon: (_row: Workflow) => { return ; - }, - onClick: (row: Workflow) => { - if (!deletingWorkflows.has(row.id)) { - handleDeleteWorkflow(row); - } } + // onClick is handled by FormGenerator for delete confirmation }, ], [t, deletingWorkflows, handleDeleteWorkflow, handleEditWorkflow, handlePlayWorkflow]); diff --git a/src/components/Workflows/workflowsTypes.ts b/src/components/Workflows/workflowsTypes.ts index 4e85880..ed986d0 100644 --- a/src/components/Workflows/workflowsTypes.ts +++ b/src/components/Workflows/workflowsTypes.ts @@ -41,7 +41,7 @@ export interface WorkflowsLogicReturn { export interface WorkflowActionConfig { label: string; icon: (row: Workflow) => React.ReactElement; - onClick: (row: Workflow) => void; + onClick?: (row: Workflow) => void; } export interface WorkflowColumnConfig { diff --git a/src/components/settings/settingsUser.module.css b/src/components/settings/settingsUser.module.css index e69de29..f239ab8 100644 --- a/src/components/settings/settingsUser.module.css +++ b/src/components/settings/settingsUser.module.css @@ -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; + } +} diff --git a/src/components/settings/settingsUser.tsx b/src/components/settings/settingsUser.tsx index e69de29..2969ba5 100644 --- a/src/components/settings/settingsUser.tsx +++ b/src/components/settings/settingsUser.tsx @@ -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(null); + const [userLoading, setUserLoading] = useState(false); + const [userError, setUserError] = useState(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 ( +
+
+ {t('settings.userinfo.loading')} +
+
+ ); + } + + return ( +
+ {userError && ( +
+ {t('settings.userinfo.error')}: {typeof userError === 'string' ? userError : 'An error occurred'} +
+ )} + + {user && ( + <> + {t('settings.userinfo')} + + {t('settings.userinfo.description')} + +
+
+ + 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} + /> +
+ +
+ + 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} + /> +
+
+ +
+
+ + 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} + /> +
+ +
+ + handlePhoneNameChange(e.target.value)} + placeholder={t('settings.userinfo.phone_name')} + title="This field is saved locally and not sent to the server" + /> +
+
+ +
+
+ + +
+ +
+ {/* Empty field to maintain layout */} +
+
+ +
+
+ + +
+ +
+ + +
+
+ + {updateMessage && ( +
+ {updateMessage.text} +
+ )} + +
+ +
+ + )} +
+ ); +} + +export default SettingsUser; diff --git a/src/hooks/privilegeCheckers.ts b/src/hooks/privilegeCheckers.ts new file mode 100644 index 0000000..b5ed033 --- /dev/null +++ b/src/hooks/privilegeCheckers.ts @@ -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 +): PrivilegeChecker => { + return async (): Promise => { + 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 | Promise> +): PrivilegeChecker => { + return async (): Promise => { + 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 +): PrivilegeChecker => { + return async (): Promise => { + 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 +): 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) +}; diff --git a/src/hooks/privilegeTestUtils.ts b/src/hooks/privilegeTestUtils.ts new file mode 100644 index 0000000..64a3a83 --- /dev/null +++ b/src/hooks/privilegeTestUtils.ts @@ -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 = { + '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 = {}; + + for (const page of pages) { + results[page] = await checkPageVisibility(page); + } + + console.log('📊 All pages visibility results:', results); + return results; +}; diff --git a/src/hooks/useAuthentication.ts b/src/hooks/useAuthentication.ts index c89f64b..501476c 100644 --- a/src/hooks/useAuthentication.ts +++ b/src/hooks/useAuthentication.ts @@ -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(null); + const [isGoogleLoading, setIsGoogleLoading] = useState(false); + + const loginWithGoogle = async (): Promise => { + 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 interface MsalRegisterData { username: string; diff --git a/src/hooks/useConnections.ts b/src/hooks/useConnections.ts index 8c13a20..df1ed6f 100644 --- a/src/hooks/useConnections.ts +++ b/src/hooks/useConnections.ts @@ -161,6 +161,46 @@ export function useConnections() { } }; + // Refresh Microsoft token + const refreshMicrosoftToken = async (connectionId: string): Promise => { + 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 => { + 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 { connections, fetchConnections, @@ -169,6 +209,8 @@ export function useConnections() { connectService, disconnectService, deleteConnection, + refreshMicrosoftToken, + refreshGoogleToken, isLoading, error }; diff --git a/src/hooks/useSidebar.ts b/src/hooks/useSidebar.ts index 81251f6..0cbfe5b 100644 --- a/src/hooks/useSidebar.ts +++ b/src/hooks/useSidebar.ts @@ -1,12 +1,14 @@ -import { useMemo, useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { getSidebarItems } from '../components/PageManager/pageConfigs'; import { SidebarItem } from '../components/PageManager/pageConfigInterface'; import { useLanguage } from '../contexts/LanguageContext'; // Hook to get sidebar items from page configurations -export const useSidebarFromPageConfigs = (): SidebarItem[] => { +export const useSidebarFromPageConfigs = (): { items: SidebarItem[]; isLoading: boolean } => { const { t } = useLanguage(); const [refreshTrigger, setRefreshTrigger] = useState(0); + const [sidebarItems, setSidebarItems] = useState([]); + const [isLoading, setIsLoading] = useState(true); // Listen for localStorage changes to refresh sidebar when sign-up status changes useEffect(() => { @@ -32,23 +34,39 @@ export const useSidebarFromPageConfigs = (): SidebarItem[] => { }; }, []); - return useMemo(() => { - console.log('🔄 Sidebar refreshing, trigger:', refreshTrigger); - const sidebarItems = getSidebarItems(); - console.log('📋 Sidebar items:', sidebarItems.map(item => ({ - name: item.name, - hasSubmenu: !!item.submenu, - submenuCount: item.submenu?.length || 0 - }))); + // Load sidebar items when refreshTrigger changes + useEffect(() => { + const loadSidebarItems = async () => { + try { + setIsLoading(true); + console.log('🔄 Sidebar refreshing, trigger:', refreshTrigger); + + const items = await getSidebarItems(); + console.log('📋 Sidebar items:', items.map(item => ({ + name: item.name, + hasSubmenu: !!item.submenu, + submenuCount: item.submenu?.length || 0 + }))); + + // 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); + } + }; - // Map the items with translations - return sidebarItems.map(item => ({ - ...item, - // You can add translations here later if needed - // For now, we'll use the names from pageConfigs directly - name: getTranslatedName(item.name, t) - })); + loadSidebarItems(); }, [t, refreshTrigger]); + + return { items: sidebarItems, isLoading }; }; // Helper function to get translated names @@ -69,4 +87,10 @@ const getTranslatedName = (name: string, t: (key: string) => string): string => 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; diff --git a/src/hooks/useUsers.ts b/src/hooks/useUsers.ts index 716c08e..8e3c59e 100644 --- a/src/hooks/useUsers.ts +++ b/src/hooks/useUsers.ts @@ -28,8 +28,12 @@ export function useCurrentUser() { method: 'get' }); setUser(data); + // Cache user data in localStorage for privilege checkers + localStorage.setItem('currentUser', JSON.stringify(data)); } catch (error) { setUser(null); + // Clear cached user data on error + localStorage.removeItem('currentUser'); } }; @@ -82,6 +86,19 @@ export function useCurrentUser() { }; 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(); }, []); @@ -135,6 +152,15 @@ export function useOrgUsers() { }); }; + const createUser = async (userData: Omit & { password: string }) => { + await request({ + url: '/api/users', + method: 'post', + data: userData + }); + await fetchUsers(); // Refresh the list after creation + }; + useEffect(() => { fetchUsers(); }, []); @@ -146,7 +172,8 @@ export function useOrgUsers() { refetch: fetchUsers, updateUser, deleteUser, - getUser + getUser, + createUser }; } diff --git a/src/locales/de.ts b/src/locales/de.ts index 111a378..a95c1e9 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -28,6 +28,8 @@ export default { 'settings.userinfo.username': 'Benutzername', 'settings.userinfo.fullname': 'Vollständiger Name', '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.privilege': 'Berechtigungsstufe', 'settings.userinfo.enabled': 'Kontostatus', @@ -153,7 +155,7 @@ export default { // Connection Actions 'connections.action.edit': 'Bearbeiten', - 'connections.action.toggle_connection': 'Verbindung umschalten', + 'connections.action.update': 'Aktualisieren', 'connections.action.delete': 'Löschen', // Prompt Modal @@ -467,11 +469,36 @@ export default { 'users.column.username': 'Benutzername', 'users.column.name': 'Name', 'users.column.email': 'E-Mail', + 'users.column.password': 'Passwort', '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.noName': 'Kein Name', 'users.noEmail': 'Keine E-Mail', '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.delete': 'Löschen', 'users.delete.confirm': 'Sind Sie sicher, dass Sie "{name}" löschen möchten?', diff --git a/src/locales/en.ts b/src/locales/en.ts index 45f15bc..0ac7899 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -28,6 +28,8 @@ export default { 'settings.userinfo.username': 'Username', 'settings.userinfo.fullname': 'Full Name', '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.privilege': 'Privilege Level', 'settings.userinfo.enabled': 'Account Status', @@ -153,7 +155,7 @@ export default { // Connection Actions 'connections.action.edit': 'Edit', - 'connections.action.toggle_connection': 'Toggle Connection', + 'connections.action.update': 'Update', 'connections.action.delete': 'Delete', @@ -467,11 +469,36 @@ export default { 'users.column.username': 'Username', 'users.column.name': 'Name', 'users.column.email': 'Email', + 'users.column.password': 'Password', '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.noName': 'No Name', 'users.noEmail': 'No Email', '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.delete': 'Delete', 'users.delete.confirm': 'Are you sure you want to delete "{name}"?', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 1da63a0..5d278d1 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -28,6 +28,8 @@ export default { 'settings.userinfo.username': 'Nom d\'utilisateur', 'settings.userinfo.fullname': 'Nom complet', '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.privilege': 'Niveau de privilège', 'settings.userinfo.enabled': 'Statut du compte', @@ -153,7 +155,7 @@ export default { // Connection Actions 'connections.action.edit': 'Modifier', - 'connections.action.toggle_connection': 'Basculer la connexion', + 'connections.action.update': 'Mettre à jour', 'connections.action.delete': 'Supprimer', // Prompt Modal @@ -467,11 +469,36 @@ export default { 'users.column.username': 'Nom d\'utilisateur', 'users.column.name': 'Nom', 'users.column.email': 'E-mail', + 'users.column.password': 'Mot de passe', '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.noName': 'Aucun nom', 'users.noEmail': 'Aucun e-mail', '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.delete': 'Supprimer', 'users.delete.confirm': 'Êtes-vous sûr de vouloir supprimer "{name}" ?', diff --git a/src/pages/Home/Einstellungen.tsx b/src/pages/Home/Einstellungen.tsx index 8101b23..4b1d8db 100644 --- a/src/pages/Home/Einstellungen.tsx +++ b/src/pages/Home/Einstellungen.tsx @@ -1,59 +1,19 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import styles from './HomeStyles/Einstellungen.module.css'; import sharedStyles from '../../components/PageManager/pages.module.css'; -import { useLanguage, Language } from '../../contexts/LanguageContext'; -import { useCurrentUser, useUser, User } from '../../hooks/useUsers'; +import { useLanguage } from '../../contexts/LanguageContext'; import SettingsSpeech from '../../components/settings/settingsSpeech'; +import SettingsUser from '../../components/settings/settingsUser'; function Einstellungen() { console.log('🏠 Einstellungen component loaded'); const [isDarkMode, setIsDarkMode] = useState(false); - const { currentLanguage, setLanguage, t, isLoading } = useLanguage(); - const { user: currentUser, isLoading: currentUserLoading } = useCurrentUser(); - const { getUser, updateUser, isLoading: updateLoading } = useUser(); + const { t, isLoading } = useLanguage(); const navigate = useNavigate(); - // Local state for user data fetched directly via API - const [user, setUser] = useState(null); - const [userLoading, setUserLoading] = useState(false); - const [userError, setUserError] = useState(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 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 const checkSpeechIntegration = () => { @@ -94,27 +54,6 @@ function Einstellungen() { 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 useEffect(() => { @@ -237,110 +176,13 @@ function Einstellungen() { 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 - }; - - 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) { + if (isLoading) { return (
- {isLoading ? t('common.loading') : t('settings.userinfo.loading')} + {t('common.loading')}
@@ -355,143 +197,7 @@ function Einstellungen() {
{/* User Information Section */} - - {userError && ( -
- {t('settings.userinfo.error')}: {typeof userError === 'string' ? userError : 'An error occurred'} -
- )} - - {user && ( -
- {t('settings.userinfo')} - - {t('settings.userinfo.description')} - -
-
- - 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} - /> -
- -
- - 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} - /> -
-
- -
-
- - 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} - /> -
- -
- - -
-
- -
-
- - -
- -
- - -
-
- - {updateMessage && ( -
- {updateMessage.text} -
- )} - -
- -
-
- )} + {/* Theme Setting */}
diff --git a/src/pages/Home/TeamBereich.tsx b/src/pages/Home/TeamBereich.tsx index 2f39d03..11f7638 100644 --- a/src/pages/Home/TeamBereich.tsx +++ b/src/pages/Home/TeamBereich.tsx @@ -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 MitgliederTable from '../../components/Mitglieder/MitgliederTable'; -function TeamBereich () { +function TeamBereich() { + const { t } = useLanguage(); + const [showAddUser, setShowAddUser] = useState(false); + + const handleAddUser = () => { + setShowAddUser(true); + }; + return (
-
-

Team-Bereich

-
-
- +
+

{t('nav.team', 'Team-Bereich')}

+ +
+
+ +
+ setShowAddUser(false)} + />
diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 95f8496..efca917 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -2,7 +2,7 @@ import { useNavigate, useLocation } from 'react-router-dom'; import { useState, useEffect } from 'react'; 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'; @@ -16,6 +16,7 @@ function Login() { const [passwordFocused, setPasswordFocused] = useState(false); const { login, error: loginError, isLoading: isLoginLoading } = useAuth(); const { loginWithMsal, error: msalError, isLoading: isMsalLoading } = useMsalAuth(); + const { loginWithGoogle, error: googleError, isLoading: isGoogleLoading } = useGoogleAuth(); // Get the page the user was trying to visit 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) => { e?.preventDefault(); // Prevent default form submission try { @@ -84,8 +96,8 @@ function Login() {
- {(loginError || msalError) && ( -
{loginError || msalError}
+ {(loginError || msalError || googleError) && ( +
{loginError || msalError || googleError}
)}
console.log("Google button clicked")} - disabled={false} + onClick={handleGoogleLogin} + disabled={isGoogleLoading} >
- Mit Google anmelden + {isGoogleLoading ? "Signing in..." : "Mit Google anmelden"}