implemented feedback
This commit is contained in:
parent
9fc33c7c73
commit
9e7c3b2aa7
36 changed files with 3055 additions and 508 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export interface ConnectionsErrorDisplayProps {
|
|||
// Table Action Interface
|
||||
export interface TableAction {
|
||||
label: string;
|
||||
onClick: (connection: Connection) => Promise<void> | void;
|
||||
onClick?: (connection: Connection) => Promise<void> | void;
|
||||
icon: React.ReactNode | ((connection: Connection) => React.ReactNode);
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ export interface ConnectionHandlers {
|
|||
handleDisconnect: (connection: Connection) => Promise<void>;
|
||||
handleDelete: (connection: Connection) => Promise<void>;
|
||||
handleDeleteMultiple: (connections: Connection[]) => Promise<void>;
|
||||
handleConnectOrDisconnect: (connection: Connection) => Promise<void>;
|
||||
handleUpdateConnection: (connection: Connection) => Promise<void>;
|
||||
handleEditConnection: (connection: Connection) => Promise<void>;
|
||||
handleSaveConnection: (updatedConnection: Connection) => Promise<void>;
|
||||
handleCancelEdit: () => void;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -25,6 +25,8 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
|
|||
createConnection,
|
||||
updateConnection,
|
||||
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: <MdModeEdit />
|
||||
},
|
||||
{
|
||||
label: t('connections.action.toggle_connection', 'Toggle Connection'),
|
||||
onClick: handleConnectOrDisconnect,
|
||||
icon: (connection: Connection) => connection.status === 'active' ? <GoUnlink /> : <IoIosLink />
|
||||
label: t('connections.action.update', 'Update'),
|
||||
onClick: handleUpdateConnection,
|
||||
icon: <IoIosRefresh />
|
||||
},
|
||||
{
|
||||
label: t('connections.action.delete', 'Delete'),
|
||||
onClick: handleDelete,
|
||||
icon: <IoIosTrash />
|
||||
icon: <IoIosTrash />,
|
||||
// onClick is handled by FormGenerator for delete confirmation
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -440,7 +478,7 @@ export function useConnectionsLogic(): ConnectionsLogicReturn {
|
|||
handleDisconnect,
|
||||
handleDelete,
|
||||
handleDeleteMultiple,
|
||||
handleConnectOrDisconnect,
|
||||
handleUpdateConnection,
|
||||
handleEditConnection,
|
||||
handleSaveConnection,
|
||||
handleCancelEdit
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export interface DateienTableProps {
|
|||
// Table Action Interface
|
||||
export interface TableAction {
|
||||
label: string;
|
||||
onClick: (file: UserFile) => Promise<void> | void;
|
||||
onClick?: (file: UserFile) => Promise<void> | void;
|
||||
icon: React.ReactNode | ((file: UserFile) => React.ReactNode);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
// Immediately remove from UI for instant feedback
|
||||
removeFileOptimistically(file.id);
|
||||
|
||||
const success = await handleFileDelete(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();
|
||||
}
|
||||
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));
|
||||
// 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();
|
||||
// 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 <IoIosTrash />;
|
||||
},
|
||||
onClick: (row: UserFile) => {
|
||||
if (!deletingFiles.has(row.id)) {
|
||||
handleDelete(row);
|
||||
}
|
||||
}
|
||||
icon: <IoIosTrash />,
|
||||
// onClick is handled by FormGenerator for delete confirmation
|
||||
}
|
||||
], [t, previewingFiles, downloadingFiles, deletingFiles, handleDownload, handleDelete]);
|
||||
], [t, previewingFiles, downloadingFiles, handleDownload, handleDelete]);
|
||||
|
||||
return {
|
||||
files,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<T = any> {
|
||||
|
|
@ -45,6 +46,7 @@ export interface FormGeneratorProps<T = any> {
|
|||
onDeleteMultiple?: (rows: T[]) => void;
|
||||
onRefresh?: () => void;
|
||||
className?: string;
|
||||
getRowDataAttributes?: (row: T, index: number) => Record<string, string>;
|
||||
}
|
||||
|
||||
export function FormGenerator<T extends Record<string, any>>({
|
||||
|
|
@ -67,7 +69,8 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
onDelete,
|
||||
onDeleteMultiple,
|
||||
onRefresh,
|
||||
className = ''
|
||||
className = '',
|
||||
getRowDataAttributes
|
||||
}: FormGeneratorProps<T>) {
|
||||
const { t } = useLanguage();
|
||||
// Auto-detect columns if not provided
|
||||
|
|
@ -115,6 +118,13 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
|
||||
|
||||
// Delete confirmation state
|
||||
const [deleteConfirmRow, setDeleteConfirmRow] = useState<number | null>(null);
|
||||
const [deletingRows, setDeletingRows] = useState<Set<number>>(new Set());
|
||||
|
||||
// Refs for action buttons containers to detect clicks outside
|
||||
const actionButtonsRefs = useRef<Map<number, HTMLDivElement>>(new Map());
|
||||
|
||||
// Refs for resizing
|
||||
const tableRef = useRef<HTMLTableElement>(null);
|
||||
const resizingColumn = useRef<string | null>(null);
|
||||
|
|
@ -132,6 +142,28 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
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<T extends Record<string, any>>({
|
|||
}
|
||||
};
|
||||
|
||||
// Handle delete confirmation
|
||||
const handleDeleteConfirm = (_row: T, index: number) => {
|
||||
setDeleteConfirmRow(index);
|
||||
};
|
||||
|
||||
// Handle delete confirmation - confirm
|
||||
const handleDeleteConfirmYes = async (row: T, index: number) => {
|
||||
if (onDelete) {
|
||||
setDeletingRows(prev => new Set(prev).add(index));
|
||||
try {
|
||||
await onDelete(row);
|
||||
// Remove from selection if it was selected
|
||||
if (selectedRows.has(index)) {
|
||||
const newSelected = new Set(selectedRows);
|
||||
newSelected.delete(index);
|
||||
setSelectedRows(newSelected);
|
||||
if (onRowSelect) {
|
||||
const selectedData = Array.from(newSelected).map(i => paginatedData[i]);
|
||||
onRowSelect(selectedData);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setDeletingRows(prev => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(index);
|
||||
return newSet;
|
||||
});
|
||||
setDeleteConfirmRow(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle delete confirmation - cancel
|
||||
const handleDeleteConfirmNo = () => {
|
||||
setDeleteConfirmRow(null);
|
||||
};
|
||||
|
||||
// Handle delete multiple items
|
||||
const handleDeleteMultiple = () => {
|
||||
if (onDeleteMultiple && selectedRows.size > 0) {
|
||||
|
|
@ -680,12 +749,17 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedData.map((row, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={`${styles.tr} ${selectedRows.has(index) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`}
|
||||
onClick={() => onRowClick?.(row, index)}
|
||||
>
|
||||
{paginatedData.map((row, index) => {
|
||||
const dataAttributes = getRowDataAttributes ? getRowDataAttributes(row, index) : {};
|
||||
return (
|
||||
<tr
|
||||
key={index}
|
||||
className={`${styles.tr} ${selectedRows.has(index) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`}
|
||||
onClick={() => onRowClick?.(row, index)}
|
||||
{...Object.fromEntries(
|
||||
Object.entries(dataAttributes).map(([key, value]) => [`data-${key}`, value])
|
||||
)}
|
||||
>
|
||||
{selectable && (
|
||||
<td className={styles.selectColumn} style={{ width: '50px', minWidth: '50px', maxWidth: '50px' }}>
|
||||
<input
|
||||
|
|
@ -711,18 +785,80 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
className={styles.actionsColumn}
|
||||
style={{ width: '120px', minWidth: '120px', maxWidth: '120px' }}
|
||||
>
|
||||
<div className={styles.actionButtons}>
|
||||
<div
|
||||
ref={(el) => {
|
||||
if (el) {
|
||||
actionButtonsRefs.current.set(index, el);
|
||||
} else {
|
||||
actionButtonsRefs.current.delete(index);
|
||||
}
|
||||
}}
|
||||
className={styles.actionButtons}
|
||||
>
|
||||
{actions.map((action, actionIndex) => {
|
||||
const actionLabel = typeof action.label === 'function' ? action.label(row) : action.label;
|
||||
const isDeleteAction = actionLabel.toLowerCase().includes('delete') ||
|
||||
actionLabel.toLowerCase().includes('löschen') ||
|
||||
actionLabel.toLowerCase().includes('supprimer') ||
|
||||
(typeof action.label === 'string' && action.label.toLowerCase().includes('delete'));
|
||||
const isConfirmingDelete = deleteConfirmRow === index && isDeleteAction;
|
||||
const isDeleting = deletingRows.has(index);
|
||||
|
||||
// Check if delete action is disabled (e.g., for system prompts)
|
||||
const isDeleteDisabled = isDeleteAction && (
|
||||
actionLabel.toLowerCase().includes('disabled') ||
|
||||
actionLabel.toLowerCase().includes('no permission') ||
|
||||
actionLabel.toLowerCase().includes('keine berechtigung')
|
||||
);
|
||||
|
||||
|
||||
if (isConfirmingDelete) {
|
||||
return (
|
||||
<div key={actionIndex} className={styles.deleteConfirmButtons}>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteConfirmYes(row, index);
|
||||
}}
|
||||
className={`${styles.actionButton} ${styles.confirmButton}`}
|
||||
title={t('formgen.delete.confirm', 'Confirm delete')}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<span className={styles.actionIcon}>
|
||||
<IoIosCheckmark />
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteConfirmNo();
|
||||
}}
|
||||
className={`${styles.actionButton} ${styles.cancelButton}`}
|
||||
title={t('formgen.delete.cancel', 'Cancel delete')}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<span className={styles.actionIcon}>
|
||||
<IoIosClose />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={actionIndex}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
action.onClick(row);
|
||||
if (isDeleteAction && !isDeleteDisabled) {
|
||||
handleDeleteConfirm(row, index);
|
||||
} else if (action.onClick) {
|
||||
action.onClick(row);
|
||||
}
|
||||
}}
|
||||
className={styles.actionButton}
|
||||
title={actionLabel}
|
||||
disabled={isDeleting || (deleteConfirmRow !== null && deleteConfirmRow !== index) || isDeleteDisabled}
|
||||
>
|
||||
{action.icon && (
|
||||
<span className={styles.actionIcon}>
|
||||
|
|
@ -735,22 +871,29 @@ export function FormGenerator<T extends Record<string, any>>({
|
|||
</div>
|
||||
</td>
|
||||
)}
|
||||
{detectedColumns.map(column => (
|
||||
<td
|
||||
key={column.key}
|
||||
className={styles.td}
|
||||
style={{
|
||||
width: columnWidths[column.key] || column.width || 150,
|
||||
minWidth: columnWidths[column.key] || column.width || 150,
|
||||
maxWidth: columnWidths[column.key] || column.width || 150
|
||||
}}
|
||||
>
|
||||
{formatCellValue(row[column.key], column, row)}
|
||||
</td>
|
||||
))}
|
||||
{detectedColumns.map(column => {
|
||||
const cellValue = row[column.key];
|
||||
const customClassName = column.cellClassName ? column.cellClassName(cellValue, row) : '';
|
||||
const combinedClassName = `${styles.td} ${customClassName}`.trim();
|
||||
|
||||
return (
|
||||
<td
|
||||
key={column.key}
|
||||
className={combinedClassName}
|
||||
style={{
|
||||
width: columnWidths[column.key] || column.width || 150,
|
||||
minWidth: columnWidths[column.key] || column.width || 150,
|
||||
maxWidth: columnWidths[column.key] || column.width || 150
|
||||
}}
|
||||
>
|
||||
{formatCellValue(cellValue, column, row)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
|
||||
</tr>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -62,3 +103,61 @@
|
|||
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;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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 (
|
||||
<div className={styles.errorState}>
|
||||
|
|
@ -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 && (
|
||||
<Popup
|
||||
isOpen={!!editingUser}
|
||||
title={t('users.edit.title', 'Edit User')}
|
||||
onClose={handleCancelEdit}
|
||||
size="medium"
|
||||
>
|
||||
<EditForm
|
||||
data={editingUser}
|
||||
fields={editFields}
|
||||
onSave={handleSaveUser}
|
||||
onCancel={handleCancelEdit}
|
||||
saveButtonText={t('common.save', 'Save')}
|
||||
cancelButtonText={t('common.cancel', 'Cancel')}
|
||||
/>
|
||||
</Popup>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Popup */}
|
||||
{deletingUser && (
|
||||
<Popup
|
||||
isOpen={!!deletingUser}
|
||||
title={t('users.delete.title', 'Delete User')}
|
||||
onClose={handleCancelDelete}
|
||||
size="small"
|
||||
actions={[
|
||||
{
|
||||
label: t('common.cancel', 'Cancel'),
|
||||
onClick: handleCancelDelete,
|
||||
variant: 'secondary'
|
||||
},
|
||||
{
|
||||
label: t('users.delete.confirm', 'Delete'),
|
||||
onClick: handleConfirmDelete,
|
||||
variant: 'danger'
|
||||
}
|
||||
]}
|
||||
>
|
||||
<div className={styles.deleteConfirmation}>
|
||||
<p>{t('users.delete.message', 'Are you sure you want to delete this user?')}</p>
|
||||
<div className={styles.userInfo}>
|
||||
<strong>{t('users.column.name', 'Name')}:</strong> {deletingUser.fullName || deletingUser.username}
|
||||
</div>
|
||||
<div className={styles.userInfo}>
|
||||
<strong>{t('users.column.email', 'Email')}:</strong> {deletingUser.email}
|
||||
</div>
|
||||
<p className={styles.warning}>
|
||||
{t('users.delete.warning', 'This action cannot be undone.')}
|
||||
</p>
|
||||
</div>
|
||||
</Popup>
|
||||
)}
|
||||
|
||||
{/* Add User Popup */}
|
||||
{showAddUser && (
|
||||
<Popup
|
||||
isOpen={showAddUser}
|
||||
title={t('users.add.title', 'Add User')}
|
||||
onClose={handleCancelAddUserOverride}
|
||||
size="medium"
|
||||
>
|
||||
<EditForm
|
||||
data={{
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
fullName: '',
|
||||
language: 'en',
|
||||
privilege: 'user',
|
||||
enabled: true
|
||||
}}
|
||||
fields={addUserFields}
|
||||
onSave={handleSaveNewUser}
|
||||
onCancel={handleCancelAddUserOverride}
|
||||
saveButtonText={t('users.add.create', 'Create User')}
|
||||
cancelButtonText={t('common.cancel', 'Cancel')}
|
||||
/>
|
||||
</Popup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<any>(null);
|
||||
const [deletingUser, setDeletingUser] = useState<any>(null);
|
||||
|
||||
// Configure columns for the users table
|
||||
const columns: UserColumnConfig[] = useMemo(() => [
|
||||
|
|
@ -85,11 +89,157 @@ export function useMitgliederLogic(): MitgliederLogicReturn {
|
|||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'privilege',
|
||||
label: t('users.column.privilege', 'Privilege'),
|
||||
type: 'enum',
|
||||
width: 120,
|
||||
minWidth: 100,
|
||||
maxWidth: 150,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterOptions: ['viewer', 'user', 'admin', 'sysadmin'],
|
||||
formatter: (value: string | undefined) => {
|
||||
const privilegeMap: Record<string, string> = {
|
||||
'viewer': t('users.privilege.viewer', 'Viewer'),
|
||||
'user': t('users.privilege.user', 'User'),
|
||||
'admin': t('users.privilege.admin', 'Admin'),
|
||||
'sysadmin': t('users.privilege.sysadmin', 'Sysadmin')
|
||||
};
|
||||
return (
|
||||
<span className="userPrivilege">
|
||||
{value ? privilegeMap[value] || value : t('users.noPrivilege', 'No Privilege')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
label: t('users.column.enabled', 'Enabled'),
|
||||
type: 'boolean',
|
||||
width: 100,
|
||||
minWidth: 80,
|
||||
maxWidth: 120,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
formatter: (value: boolean | undefined) => (
|
||||
<span className={`userEnabled ${value ? 'enabled' : 'disabled'}`}>
|
||||
{value ? t('users.enabled.yes', 'Yes') : t('users.enabled.no', 'No')}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'authenticationAuthority',
|
||||
label: t('users.column.authAuthority', 'Auth Authority'),
|
||||
type: 'enum',
|
||||
width: 150,
|
||||
minWidth: 120,
|
||||
maxWidth: 200,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterOptions: ['local', 'msft'],
|
||||
formatter: (value: string | undefined) => {
|
||||
const authMap: Record<string, string> = {
|
||||
'local': t('users.auth.local', 'Local'),
|
||||
'msft': t('users.auth.msft', 'Microsoft')
|
||||
};
|
||||
return (
|
||||
<span className="userAuthAuthority">
|
||||
{value ? authMap[value] || value : t('users.noAuthAuthority', 'No Auth Authority')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
], [t]);
|
||||
|
||||
// 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: () => <MdModeEdit />,
|
||||
onClick: (row: any) => handleEditUser(row)
|
||||
},
|
||||
{
|
||||
label: t('users.actions.delete', 'Delete'),
|
||||
icon: () => <IoIosTrash />,
|
||||
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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
handleCancelEdit: () => void;
|
||||
|
||||
// Delete functionality
|
||||
deletingUser: User | null;
|
||||
setDeletingUser: (user: User | null) => void;
|
||||
handleConfirmDelete: () => Promise<void>;
|
||||
handleCancelDelete: () => void;
|
||||
|
||||
// Add user functionality
|
||||
handleSaveNewUser: (userData: any) => Promise<void>;
|
||||
handleCancelAddUser: () => void;
|
||||
}
|
||||
|
|
|
|||
128
src/components/PageManager/PRIVILEGE_IMPLEMENTATION.md
Normal file
128
src/components/PageManager/PRIVILEGE_IMPLEMENTATION.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# Privilege System Implementation Summary
|
||||
|
||||
## Overview
|
||||
A comprehensive privilege checking system has been implemented for the Page Manager that integrates with the backend user data while maintaining localStorage for specific features like speech signup.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. 4-Level Privilege System
|
||||
- **viewer**: Basic read-only access
|
||||
- **user**: Standard user access
|
||||
- **admin**: Administrative access
|
||||
- **sysadmin**: System administrator access
|
||||
|
||||
### 2. Backend Integration
|
||||
- User privilege data comes from the backend via `/api/local/me` endpoint
|
||||
- User data is cached in localStorage for performance
|
||||
- Privilege checkers use real user data from the backend
|
||||
|
||||
### 3. Page Access Control
|
||||
- **All pages visible to all privilege levels**: dashboard, dateien, prompts, connections, workflows, einstellungen, speech
|
||||
- **Admin-only pages**: team-bereich (only visible to admin and sysadmin)
|
||||
- **Feature-specific access**: speech/transcripts (requires speech signup in localStorage)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Files Modified
|
||||
|
||||
#### `src/hooks/privilegeCheckers.ts`
|
||||
- Updated to use real user privilege data from localStorage cache
|
||||
- Maintains localStorage-based checkers for features not yet integrated with backend
|
||||
- Added comprehensive privilege checker functions
|
||||
|
||||
#### `src/hooks/useUsers.ts`
|
||||
- Added localStorage caching of user data for privilege checkers
|
||||
- Improved initialization to load cached data first, then fetch fresh data
|
||||
- Maintains existing logout functionality
|
||||
|
||||
#### `src/components/PageManager/pageConfigInterface.ts`
|
||||
- Added `privilegeChecker` attribute for main page access control
|
||||
- Added `subpagePrivilegeChecker` attribute for subpage access control
|
||||
|
||||
#### `src/components/PageManager/pageConfigs.ts`
|
||||
- Updated all page configurations with appropriate privilege checkers
|
||||
- Set team-bereich to admin-only access
|
||||
- Set all other pages to viewer-level access (all users)
|
||||
|
||||
### Privilege Checkers Available
|
||||
|
||||
```typescript
|
||||
// Role-based checkers (using backend user data)
|
||||
privilegeCheckers.viewerRole // viewer, user, admin, sysadmin
|
||||
privilegeCheckers.userRole // user, admin, sysadmin
|
||||
privilegeCheckers.adminRole // admin, sysadmin
|
||||
privilegeCheckers.sysadminRole // sysadmin only
|
||||
|
||||
// Feature-based checkers (using localStorage)
|
||||
privilegeCheckers.speechSignup // Speech signup (localStorage)
|
||||
privilegeCheckers.authenticated // Any authenticated user
|
||||
privilegeCheckers.alwaysAllow // Always visible
|
||||
privilegeCheckers.neverAllow // Never visible
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Setting User Privilege for Testing
|
||||
```typescript
|
||||
import { simulateUserPrivilege, testAllPagesVisibility } from '../hooks/privilegeTestUtils';
|
||||
|
||||
// Simulate a user with admin privilege
|
||||
simulateUserPrivilege('admin');
|
||||
|
||||
// Test all pages visibility
|
||||
await testAllPagesVisibility();
|
||||
```
|
||||
|
||||
### Adding Privilege to New Pages
|
||||
```typescript
|
||||
{
|
||||
path: 'new-page',
|
||||
component: NewPage,
|
||||
// ... other config
|
||||
privilegeChecker: privilegeCheckers.adminRole, // Only admins can access
|
||||
}
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. **User Login**: Backend returns user data with privilege level
|
||||
2. **Caching**: User data is cached in localStorage as 'currentUser'
|
||||
3. **Privilege Checking**: Privilege checkers read from localStorage cache
|
||||
4. **Page Visibility**: getSidebarItems() checks privileges and filters visible pages
|
||||
5. **Real-time Updates**: User data is refreshed on each app load
|
||||
|
||||
## Testing
|
||||
|
||||
Use the provided test utilities in `src/hooks/privilegeTestUtils.ts`:
|
||||
|
||||
```typescript
|
||||
// Test all privilege checkers
|
||||
await testAllPrivilegeCheckers();
|
||||
|
||||
// Simulate different privilege levels
|
||||
simulateUserPrivilege('viewer');
|
||||
simulateUserPrivilege('user');
|
||||
simulateUserPrivilege('admin');
|
||||
simulateUserPrivilege('sysadmin');
|
||||
|
||||
// Test specific page visibility
|
||||
await checkPageVisibility('team-bereich');
|
||||
|
||||
// Test all pages
|
||||
await testAllPagesVisibility();
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Client-side privilege checking is for UI/UX purposes only
|
||||
- Backend must enforce actual access control for sensitive operations
|
||||
- User data is cached locally but refreshed on each app load
|
||||
- Speech signup remains in localStorage as it's not yet backend-integrated
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Real-time privilege updates without page refresh
|
||||
- Server-side privilege validation
|
||||
- More granular permission levels
|
||||
- Privilege inheritance and delegation
|
||||
- Audit logging for privilege changes
|
||||
245
src/components/PageManager/README.md
Normal file
245
src/components/PageManager/README.md
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
# Generic Subpage System
|
||||
|
||||
This system provides a flexible, reusable way to create pages with conditional subpages in the sidebar. It replaces the hardcoded speech-specific logic with a generic approach that can be applied to any page.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Generic subpage support**: Any page can have subpages by setting `hasSubpages: true`
|
||||
- **Flexible privilege checking**: Multiple built-in privilege checkers plus support for custom ones
|
||||
- **Automatic submenu generation**: Subpages are automatically found and organized into submenus
|
||||
- **Conditional display**: Subpages only show when privilege conditions are met
|
||||
- **Async support**: Privilege checkers can be async for complex logic
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Page Configuration
|
||||
|
||||
To create a page with subpages, add these properties to your `PageConfig`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: 'parent-page',
|
||||
component: YourComponent,
|
||||
// ... other properties
|
||||
|
||||
// Generic subpage support
|
||||
hasSubpages: true, // Enable subpage support
|
||||
subpagePrefix: 'parent-page', // Prefix to identify subpages
|
||||
privilegeChecker: privilegeCheckers.yourChecker, // When to show subpages
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Subpage Configuration
|
||||
|
||||
Subpages are regular page configs with these requirements:
|
||||
|
||||
```typescript
|
||||
{
|
||||
path: 'parent-page/subpage', // Must start with parent prefix
|
||||
component: SubpageComponent,
|
||||
// ... other properties
|
||||
|
||||
showInSidebar: false, // Important: prevents main sidebar display
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Privilege Checkers
|
||||
|
||||
The system includes several built-in privilege checkers:
|
||||
|
||||
#### LocalStorage with Expiration
|
||||
```typescript
|
||||
privilegeCheckers.speechSignup // 24-hour localStorage data
|
||||
privilegeCheckers.premiumUser // 30-day localStorage data
|
||||
```
|
||||
|
||||
#### Role-Based
|
||||
```typescript
|
||||
privilegeCheckers.adminRole // Check for admin/super-admin roles
|
||||
```
|
||||
|
||||
#### Feature Flags
|
||||
```typescript
|
||||
privilegeCheckers.betaFeatures // Check feature flag status
|
||||
```
|
||||
|
||||
#### Authentication
|
||||
```typescript
|
||||
privilegeCheckers.authenticated // Check if user is logged in
|
||||
```
|
||||
|
||||
#### Custom Checkers
|
||||
```typescript
|
||||
const customChecker = createCustomPrivilegeChecker(async () => {
|
||||
// Your custom logic
|
||||
return someCondition;
|
||||
});
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Admin Section
|
||||
|
||||
```typescript
|
||||
// Main admin page
|
||||
{
|
||||
path: 'admin',
|
||||
component: AdminDashboard,
|
||||
id: 'admin',
|
||||
name: 'Admin',
|
||||
icon: AdminIcon,
|
||||
order: 10,
|
||||
showInSidebar: true,
|
||||
hasSubpages: true,
|
||||
subpagePrefix: 'admin',
|
||||
privilegeChecker: privilegeCheckers.adminRole,
|
||||
}
|
||||
|
||||
// Admin subpages
|
||||
{
|
||||
path: 'admin/users',
|
||||
component: AdminUsers,
|
||||
id: 'admin-users',
|
||||
name: 'User Management',
|
||||
showInSidebar: false, // Important!
|
||||
},
|
||||
{
|
||||
path: 'admin/settings',
|
||||
component: AdminSettings,
|
||||
id: 'admin-settings',
|
||||
name: 'System Settings',
|
||||
showInSidebar: false, // Important!
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Premium Features
|
||||
|
||||
```typescript
|
||||
// Main premium page
|
||||
{
|
||||
path: 'premium',
|
||||
component: PremiumDashboard,
|
||||
id: 'premium',
|
||||
name: 'Premium',
|
||||
icon: PremiumIcon,
|
||||
order: 11,
|
||||
showInSidebar: true,
|
||||
hasSubpages: true,
|
||||
subpagePrefix: 'premium',
|
||||
privilegeChecker: privilegeCheckers.premiumUser,
|
||||
}
|
||||
|
||||
// Premium subpages
|
||||
{
|
||||
path: 'premium/advanced-tools',
|
||||
component: AdvancedTools,
|
||||
id: 'premium-advanced',
|
||||
name: 'Advanced Tools',
|
||||
showInSidebar: false,
|
||||
},
|
||||
{
|
||||
path: 'premium/priority-support',
|
||||
component: PrioritySupport,
|
||||
id: 'premium-support',
|
||||
name: 'Priority Support',
|
||||
showInSidebar: false,
|
||||
}
|
||||
```
|
||||
|
||||
## Migration from Hardcoded System
|
||||
|
||||
The old hardcoded speech system has been replaced with the generic system:
|
||||
|
||||
### Before (Hardcoded)
|
||||
```typescript
|
||||
// Special handling in getSidebarItems()
|
||||
if (config.path === 'speech') {
|
||||
const hasSignedUp = checkSpeechSignUpStatus();
|
||||
// ... hardcoded logic
|
||||
}
|
||||
```
|
||||
|
||||
### After (Generic)
|
||||
```typescript
|
||||
// Speech page config
|
||||
{
|
||||
path: 'speech',
|
||||
component: Speech,
|
||||
hasSubpages: true,
|
||||
subpagePrefix: 'speech',
|
||||
privilegeChecker: privilegeCheckers.speechSignup,
|
||||
}
|
||||
|
||||
// Speech subpage
|
||||
{
|
||||
path: 'speech/transcripts',
|
||||
component: SpeechTranscripts,
|
||||
showInSidebar: false,
|
||||
}
|
||||
```
|
||||
|
||||
## Adding New Subpages
|
||||
|
||||
1. **Create the main page config** with subpage support:
|
||||
```typescript
|
||||
{
|
||||
path: 'your-page',
|
||||
hasSubpages: true,
|
||||
subpagePrefix: 'your-page',
|
||||
privilegeChecker: privilegeCheckers.yourChecker,
|
||||
}
|
||||
```
|
||||
|
||||
2. **Create subpage configs**:
|
||||
```typescript
|
||||
{
|
||||
path: 'your-page/subpage1',
|
||||
component: Subpage1Component,
|
||||
showInSidebar: false,
|
||||
},
|
||||
{
|
||||
path: 'your-page/subpage2',
|
||||
component: Subpage2Component,
|
||||
showInSidebar: false,
|
||||
}
|
||||
```
|
||||
|
||||
3. **Add to pageConfigs array** - the system will automatically handle the rest!
|
||||
|
||||
## Benefits
|
||||
|
||||
- **No more hardcoded logic**: Each page handles its own subpage logic
|
||||
- **Reusable privilege checkers**: Common patterns are built-in
|
||||
- **Easy to extend**: Adding new subpages is just configuration
|
||||
- **Type-safe**: Full TypeScript support
|
||||
- **Async support**: Complex privilege logic can be async
|
||||
- **Error handling**: Graceful fallbacks when privilege checks fail
|
||||
|
||||
## API Reference
|
||||
|
||||
### PageConfig Properties
|
||||
|
||||
- `hasSubpages?: boolean` - Enable subpage support
|
||||
- `subpagePrefix?: string` - Prefix to identify subpages
|
||||
- `privilegeChecker?: PrivilegeChecker` - Function to check privileges
|
||||
|
||||
### PrivilegeChecker Type
|
||||
|
||||
```typescript
|
||||
type PrivilegeChecker = () => boolean | Promise<boolean>;
|
||||
```
|
||||
|
||||
### Built-in Privilege Checkers
|
||||
|
||||
- `privilegeCheckers.speechSignup` - 24-hour localStorage data
|
||||
- `privilegeCheckers.premiumUser` - 30-day localStorage data
|
||||
- `privilegeCheckers.adminRole` - Admin role check
|
||||
- `privilegeCheckers.betaFeatures` - Feature flag check
|
||||
- `privilegeCheckers.authenticated` - Authentication check
|
||||
|
||||
### Helper Functions
|
||||
|
||||
- `createLocalStoragePrivilegeChecker(dataKey, timestampKey, expirationHours)` - Create localStorage-based checker
|
||||
- `createRolePrivilegeChecker(requiredRoles, getUserRoles)` - Create role-based checker
|
||||
- `createFeatureFlagChecker(featureFlag, getFeatureFlags)` - Create feature flag checker
|
||||
- `createCustomPrivilegeChecker(checkFunction)` - Create custom checker
|
||||
|
|
@ -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<boolean>;
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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))) {
|
||||
|
||||
if (hasSignedUp) {
|
||||
// Find the transcript subpage
|
||||
const transcriptConfig = pageConfigs.find(c => c.path === 'speech/transcripts');
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
if (transcriptConfig) {
|
||||
// Create expandable Speech item with submenu
|
||||
// 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 (hasSubpagePrivilege) {
|
||||
// Find all subpages for this parent
|
||||
const subpages = pageConfigs.filter(c =>
|
||||
c.path.startsWith(`${config.subpagePrefix}/`) &&
|
||||
c.path !== config.subpagePrefix &&
|
||||
c.showInSidebar === false // Subpages should not show as main items
|
||||
);
|
||||
|
||||
if (subpages.length > 0) {
|
||||
// Create expandable item with submenu
|
||||
items.push({
|
||||
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;
|
||||
|
|
|
|||
234
src/components/PageManager/subpageExamples.ts
Normal file
234
src/components/PageManager/subpageExamples.ts
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
// Example configurations showing how to add subpages with the new generic system
|
||||
|
||||
import { PageConfig } from './pageConfigInterface';
|
||||
import { privilegeCheckers, createCustomPrivilegeChecker } from '../../hooks/privilegeCheckers';
|
||||
import { lazy } from 'react';
|
||||
|
||||
// Example 1: Admin section with multiple subpages
|
||||
export const adminPageConfig: PageConfig = {
|
||||
path: 'admin',
|
||||
component: lazy(() => import('../../pages/Home/Admin')),
|
||||
persistent: false,
|
||||
preload: true,
|
||||
moduleEnabled: true,
|
||||
id: 'admin',
|
||||
name: 'Admin',
|
||||
icon: () => null, // Your admin icon
|
||||
order: 10,
|
||||
showInSidebar: true,
|
||||
// Generic subpage support
|
||||
hasSubpages: true,
|
||||
subpagePrefix: 'admin',
|
||||
privilegeChecker: privilegeCheckers.adminRole, // Only show subpages to admins
|
||||
onActivate: async () => {
|
||||
console.log('Admin section activated');
|
||||
}
|
||||
};
|
||||
|
||||
// Example 2: Premium features with expiration-based access
|
||||
export const premiumPageConfig: PageConfig = {
|
||||
path: 'premium',
|
||||
component: lazy(() => import('../../pages/Home/Premium')),
|
||||
persistent: false,
|
||||
preload: true,
|
||||
moduleEnabled: true,
|
||||
id: 'premium',
|
||||
name: 'Premium',
|
||||
icon: () => null, // Your premium icon
|
||||
order: 11,
|
||||
showInSidebar: true,
|
||||
// Generic subpage support
|
||||
hasSubpages: true,
|
||||
subpagePrefix: 'premium',
|
||||
privilegeChecker: privilegeCheckers.premiumUser, // Only show subpages to premium users
|
||||
onActivate: async () => {
|
||||
console.log('Premium section activated');
|
||||
}
|
||||
};
|
||||
|
||||
// Example 3: Beta features with feature flag
|
||||
export const betaPageConfig: PageConfig = {
|
||||
path: 'beta',
|
||||
component: lazy(() => import('../../pages/Home/Beta')),
|
||||
persistent: false,
|
||||
preload: false,
|
||||
moduleEnabled: true,
|
||||
id: 'beta',
|
||||
name: 'Beta Features',
|
||||
icon: () => null, // Your beta icon
|
||||
order: 12,
|
||||
showInSidebar: true,
|
||||
// Generic subpage support
|
||||
hasSubpages: true,
|
||||
subpagePrefix: 'beta',
|
||||
privilegeChecker: privilegeCheckers.betaFeatures, // Only show subpages if beta features are enabled
|
||||
onActivate: async () => {
|
||||
console.log('Beta section activated');
|
||||
}
|
||||
};
|
||||
|
||||
// Example 4: Custom privilege checker
|
||||
const customPrivilegeChecker = createCustomPrivilegeChecker(async () => {
|
||||
// Your custom logic here
|
||||
const userLevel = localStorage.getItem('userLevel');
|
||||
const hasSpecialAccess = userLevel === 'vip' || userLevel === 'premium';
|
||||
|
||||
console.log('Custom privilege check:', { userLevel, hasSpecialAccess });
|
||||
return hasSpecialAccess;
|
||||
});
|
||||
|
||||
export const customPageConfig: PageConfig = {
|
||||
path: 'custom',
|
||||
component: lazy(() => import('../../pages/Home/Custom')),
|
||||
persistent: false,
|
||||
preload: true,
|
||||
moduleEnabled: true,
|
||||
id: 'custom',
|
||||
name: 'Custom Features',
|
||||
icon: () => null, // Your custom icon
|
||||
order: 13,
|
||||
showInSidebar: true,
|
||||
// Generic subpage support
|
||||
hasSubpages: true,
|
||||
subpagePrefix: 'custom',
|
||||
privilegeChecker: customPrivilegeChecker,
|
||||
onActivate: async () => {
|
||||
console.log('Custom section activated');
|
||||
}
|
||||
};
|
||||
|
||||
// Example subpage configurations
|
||||
export const exampleSubpages: PageConfig[] = [
|
||||
// Admin subpages
|
||||
{
|
||||
path: 'admin/users',
|
||||
component: lazy(() => import('../../pages/Home/AdminUsers')),
|
||||
persistent: false,
|
||||
preload: false,
|
||||
moduleEnabled: true,
|
||||
id: 'admin-users',
|
||||
name: 'User Management',
|
||||
icon: () => null,
|
||||
order: 1,
|
||||
showInSidebar: false, // Important: subpages should not show as main items
|
||||
onActivate: async () => {
|
||||
console.log('Admin Users activated');
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'admin/settings',
|
||||
component: lazy(() => import('../../pages/Home/AdminSettings')),
|
||||
persistent: false,
|
||||
preload: false,
|
||||
moduleEnabled: true,
|
||||
id: 'admin-settings',
|
||||
name: 'System Settings',
|
||||
icon: () => null,
|
||||
order: 2,
|
||||
showInSidebar: false,
|
||||
onActivate: async () => {
|
||||
console.log('Admin Settings activated');
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'admin/analytics',
|
||||
component: lazy(() => import('../../pages/Home/AdminAnalytics')),
|
||||
persistent: false,
|
||||
preload: false,
|
||||
moduleEnabled: true,
|
||||
id: 'admin-analytics',
|
||||
name: 'Analytics',
|
||||
icon: () => null,
|
||||
order: 3,
|
||||
showInSidebar: false,
|
||||
onActivate: async () => {
|
||||
console.log('Admin Analytics activated');
|
||||
}
|
||||
},
|
||||
|
||||
// Premium subpages
|
||||
{
|
||||
path: 'premium/advanced-tools',
|
||||
component: lazy(() => import('../../pages/Home/PremiumAdvancedTools')),
|
||||
persistent: false,
|
||||
preload: false,
|
||||
moduleEnabled: true,
|
||||
id: 'premium-advanced',
|
||||
name: 'Advanced Tools',
|
||||
icon: () => null,
|
||||
order: 1,
|
||||
showInSidebar: false,
|
||||
onActivate: async () => {
|
||||
console.log('Premium Advanced Tools activated');
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'premium/priority-support',
|
||||
component: lazy(() => import('../../pages/Home/PremiumSupport')),
|
||||
persistent: false,
|
||||
preload: false,
|
||||
moduleEnabled: true,
|
||||
id: 'premium-support',
|
||||
name: 'Priority Support',
|
||||
icon: () => null,
|
||||
order: 2,
|
||||
showInSidebar: false,
|
||||
onActivate: async () => {
|
||||
console.log('Premium Support activated');
|
||||
}
|
||||
},
|
||||
|
||||
// Beta subpages
|
||||
{
|
||||
path: 'beta/experimental-features',
|
||||
component: lazy(() => import('../../pages/Home/BetaExperimental')),
|
||||
persistent: false,
|
||||
preload: false,
|
||||
moduleEnabled: true,
|
||||
id: 'beta-experimental',
|
||||
name: 'Experimental Features',
|
||||
icon: () => null,
|
||||
order: 1,
|
||||
showInSidebar: false,
|
||||
onActivate: async () => {
|
||||
console.log('Beta Experimental Features activated');
|
||||
}
|
||||
},
|
||||
|
||||
// Custom subpages
|
||||
{
|
||||
path: 'custom/special-tools',
|
||||
component: lazy(() => import('../../pages/Home/CustomSpecialTools')),
|
||||
persistent: false,
|
||||
preload: false,
|
||||
moduleEnabled: true,
|
||||
id: 'custom-special',
|
||||
name: 'Special Tools',
|
||||
icon: () => null,
|
||||
order: 1,
|
||||
showInSidebar: false,
|
||||
onActivate: async () => {
|
||||
console.log('Custom Special Tools activated');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// How to add these to your main pageConfigs array:
|
||||
/*
|
||||
// In your main pageConfigs.ts file, you would add:
|
||||
|
||||
// 1. Add the main page configs
|
||||
...adminPageConfig,
|
||||
...premiumPageConfig,
|
||||
...betaPageConfig,
|
||||
...customPageConfig,
|
||||
|
||||
// 2. Add the subpage configs
|
||||
...exampleSubpages,
|
||||
|
||||
// The system will automatically:
|
||||
// - Detect pages with hasSubpages: true
|
||||
// - Find subpages with matching subpagePrefix
|
||||
// - Check privileges using the privilegeChecker
|
||||
// - Show/hide submenus accordingly
|
||||
*/
|
||||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import { GoSidebarExpand, GoSidebarCollapse } from 'react-icons/go';
|
|||
const Sidebar: React.FC<SidebarProps> = ({ data }) => {
|
||||
const sidebar = useSidebarLogic();
|
||||
|
||||
// Ensure data is always an array
|
||||
const sidebarItems = Array.isArray(data) ? data : [];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.sidebarContainer} ${sidebar.state.isMinimized ? styles.minimized : ''}`}
|
||||
|
|
@ -40,7 +43,7 @@ const Sidebar: React.FC<SidebarProps> = ({ data }) => {
|
|||
<div
|
||||
className={styles.sidebar}
|
||||
>
|
||||
{data.map(item => {
|
||||
{sidebarItems.map(item => {
|
||||
return (
|
||||
<SidebarItem
|
||||
key={item.id}
|
||||
|
|
@ -62,7 +65,28 @@ const Sidebar: React.FC<SidebarProps> = ({ data }) => {
|
|||
}
|
||||
|
||||
const SidebarWithData: React.FC = () => {
|
||||
const sidebarData = useSidebarFromPageConfigs();
|
||||
const { items: sidebarData, isLoading } = useSidebarFromPageConfigs();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={`${styles.sidebarContainer}`}>
|
||||
<div className={styles.logoContainer}>
|
||||
<div className={styles.logoWrapper}>
|
||||
<div className={styles.logoText}>
|
||||
<span className={styles.logoPower}>Power</span>
|
||||
<span className={styles.logoOn}>On</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.sidebar}>
|
||||
<div style={{ padding: '20px', textAlign: 'center' }}>
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <Sidebar data={sidebarData} />;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export interface TestSharepointTableProps {
|
|||
// Table Action Interface
|
||||
export interface TableAction {
|
||||
label: string;
|
||||
onClick: (document: SharePointDocument) => Promise<void> | void;
|
||||
onClick?: (document: SharePointDocument) => Promise<void> | void;
|
||||
icon: React.ReactNode | ((document: SharePointDocument) => React.ReactNode);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -417,12 +417,8 @@ export function useWorkflowsLogic(): WorkflowsLogicReturn {
|
|||
label: t('workflows.action.delete'),
|
||||
icon: (_row: Workflow) => {
|
||||
return <IoIosTrash />;
|
||||
},
|
||||
onClick: (row: Workflow) => {
|
||||
if (!deletingWorkflows.has(row.id)) {
|
||||
handleDeleteWorkflow(row);
|
||||
}
|
||||
}
|
||||
// onClick is handled by FormGenerator for delete confirmation
|
||||
},
|
||||
], [t, deletingWorkflows, handleDeleteWorkflow, handleEditWorkflow, handlePlayWorkflow]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,388 @@
|
|||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useLanguage, Language } from '../../contexts/LanguageContext';
|
||||
import { useCurrentUser, useUser, User } from '../../hooks/useUsers';
|
||||
import styles from './settingsUser.module.css';
|
||||
|
||||
// Add a check to ensure we're in the right context
|
||||
const isClient = typeof window !== 'undefined';
|
||||
|
||||
interface SettingsUserProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function SettingsUser({ className }: SettingsUserProps) {
|
||||
console.log('👤 SettingsUser component loaded');
|
||||
const { currentLanguage, setLanguage, t } = useLanguage();
|
||||
const { user: currentUser } = useCurrentUser();
|
||||
const { getUser, updateUser, isLoading: updateLoading } = useUser();
|
||||
|
||||
// Local state for user data fetched directly via API
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [userLoading, setUserLoading] = useState(false);
|
||||
const [userError, setUserError] = useState<string | null>(null);
|
||||
|
||||
// Form state for user info
|
||||
const [userForm, setUserForm] = useState({
|
||||
username: '',
|
||||
fullName: '',
|
||||
email: '',
|
||||
language: 'en' as Language,
|
||||
privilege: '',
|
||||
enabled: true
|
||||
});
|
||||
|
||||
// Phone name state (stored in localStorage only)
|
||||
const [phoneName, setPhoneName] = useState('');
|
||||
const [updateMessage, setUpdateMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
||||
const [isUpdating, setIsUpdating] = useState(false); // Flag to prevent form reset during update
|
||||
const hasLoadedUser = useRef(false);
|
||||
|
||||
// Load phone name from localStorage
|
||||
const loadPhoneName = () => {
|
||||
try {
|
||||
const savedPhoneName = localStorage.getItem('userPhoneName');
|
||||
if (savedPhoneName) {
|
||||
setPhoneName(savedPhoneName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load phone name from localStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Save phone name to localStorage
|
||||
const savePhoneName = (name: string) => {
|
||||
try {
|
||||
if (name.trim()) {
|
||||
localStorage.setItem('userPhoneName', name.trim());
|
||||
} else {
|
||||
localStorage.removeItem('userPhoneName');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save phone name to localStorage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch user data directly using the /api/users/{userId} endpoint
|
||||
const fetchUserData = async () => {
|
||||
if (!currentUser?.id || hasLoadedUser.current) return;
|
||||
|
||||
hasLoadedUser.current = true;
|
||||
setUserLoading(true);
|
||||
setUserError(null);
|
||||
|
||||
try {
|
||||
const userData = await getUser(currentUser.id);
|
||||
setUser(userData);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user data:', error);
|
||||
setUserError(typeof error === 'string' ? error : 'Failed to load user data');
|
||||
hasLoadedUser.current = false; // Reset on error to allow retry
|
||||
} finally {
|
||||
setUserLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load phone name from localStorage on component mount
|
||||
useEffect(() => {
|
||||
loadPhoneName();
|
||||
}, []);
|
||||
|
||||
// Fetch user data when currentUser is available
|
||||
useEffect(() => {
|
||||
if (currentUser?.id && !hasLoadedUser.current) {
|
||||
fetchUserData();
|
||||
}
|
||||
}, [currentUser?.id]);
|
||||
|
||||
// Update form when user data is loaded (but not during an active update)
|
||||
useEffect(() => {
|
||||
if (user && !isUpdating) {
|
||||
console.log('🔄 Updating form with user data:', user);
|
||||
setUserForm({
|
||||
username: user.username || '',
|
||||
fullName: user.fullName || '',
|
||||
email: user.email || '',
|
||||
language: (user.language as Language) || currentLanguage,
|
||||
privilege: user.privilege || '',
|
||||
enabled: user.enabled
|
||||
});
|
||||
}
|
||||
}, [user, currentLanguage, isUpdating]);
|
||||
|
||||
const handleUserFormChange = (field: keyof typeof userForm, value: string | boolean) => {
|
||||
setUserForm(prev => ({ ...prev, [field]: value }));
|
||||
setUpdateMessage(null); // Clear any previous messages
|
||||
|
||||
// Language change will be handled when the form is submitted, not immediately
|
||||
};
|
||||
|
||||
const handlePhoneNameChange = (value: string) => {
|
||||
setPhoneName(value);
|
||||
savePhoneName(value); // Save to localStorage immediately
|
||||
};
|
||||
|
||||
const handleSaveUserInfo = async () => {
|
||||
if (!user) return;
|
||||
|
||||
setIsUpdating(true); // Prevent form reset during update
|
||||
|
||||
try {
|
||||
// Create complete User object with updated form data
|
||||
// Only include editable fields based on authentication authority
|
||||
const completeUserData: User = {
|
||||
id: user.id,
|
||||
username: user.authenticationAuthority === 'local' ? userForm.username : user.username,
|
||||
fullName: user.authenticationAuthority === 'local' ? userForm.fullName : user.fullName,
|
||||
email: user.authenticationAuthority === 'local' ? userForm.email : user.email,
|
||||
language: userForm.language, // Language is always editable
|
||||
privilege: userForm.privilege,
|
||||
enabled: userForm.enabled,
|
||||
authenticationAuthority: user.authenticationAuthority,
|
||||
mandateId: user.mandateId
|
||||
};
|
||||
|
||||
// Update user via API - this returns the updated user
|
||||
const updatedUser = await updateUser(user.id, completeUserData);
|
||||
|
||||
if (updatedUser) {
|
||||
console.log('✅ User update successful:', updatedUser);
|
||||
|
||||
// Update local user state with the returned data
|
||||
setUser(updatedUser);
|
||||
|
||||
// Update frontend language if it was changed
|
||||
const newLanguage = updatedUser.language as Language;
|
||||
if (newLanguage && newLanguage !== currentLanguage) {
|
||||
try {
|
||||
await setLanguage(newLanguage);
|
||||
console.log('🌍 Frontend language updated to:', newLanguage);
|
||||
} catch (error) {
|
||||
console.error('Failed to change frontend language:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Success: Update form with the actual returned data to ensure consistency
|
||||
setUserForm({
|
||||
username: updatedUser.username || '',
|
||||
fullName: updatedUser.fullName || '',
|
||||
email: updatedUser.email || '',
|
||||
language: newLanguage || currentLanguage,
|
||||
privilege: updatedUser.privilege || '',
|
||||
enabled: updatedUser.enabled
|
||||
});
|
||||
|
||||
console.log('📝 Form updated with new data');
|
||||
|
||||
// Dispatch event to notify other components (like sidebar) that user data was updated
|
||||
window.dispatchEvent(new CustomEvent('userInfoUpdated'));
|
||||
|
||||
setUpdateMessage({ type: 'success', text: t('settings.userinfo.success') });
|
||||
} else {
|
||||
throw new Error('No updated user data returned from server');
|
||||
}
|
||||
|
||||
// Clear message after 3 seconds
|
||||
setTimeout(() => setUpdateMessage(null), 3000);
|
||||
} catch (error) {
|
||||
console.error('Failed to update user info:', error);
|
||||
setUpdateMessage({ type: 'error', text: t('settings.userinfo.update_error') });
|
||||
|
||||
// Reset form to original user data on error
|
||||
if (user) {
|
||||
setUserForm({
|
||||
username: user.username || '',
|
||||
fullName: user.fullName || '',
|
||||
email: user.email || '',
|
||||
language: (user.language as Language) || currentLanguage,
|
||||
privilege: user.privilege || '',
|
||||
enabled: user.enabled
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsUpdating(false); // Re-enable form sync
|
||||
}
|
||||
};
|
||||
|
||||
const getLanguageLabel = (lang: Language): string => {
|
||||
switch (lang) {
|
||||
case 'de': return t('language.german');
|
||||
case 'en': return t('language.english');
|
||||
case 'fr': return t('language.french');
|
||||
default: return lang;
|
||||
}
|
||||
};
|
||||
|
||||
if (userLoading) {
|
||||
return (
|
||||
<div className={`${styles.userInfoForm} ${className || ''}`}>
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
{t('settings.userinfo.loading')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.userInfoForm} ${className || ''}`}>
|
||||
{userError && (
|
||||
<div className={styles.errorMessage}>
|
||||
{t('settings.userinfo.error')}: {typeof userError === 'string' ? userError : 'An error occurred'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user && (
|
||||
<>
|
||||
<span className={styles.settingLabel}>{t('settings.userinfo')}</span>
|
||||
<span className={styles.settingDescription}>
|
||||
{t('settings.userinfo.description')}
|
||||
</span>
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('settings.userinfo.username')}
|
||||
{user.authenticationAuthority !== 'local' && (
|
||||
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={userForm.username}
|
||||
onChange={(e) => handleUserFormChange('username', e.target.value)}
|
||||
placeholder={t('settings.userinfo.username')}
|
||||
readOnly={user.authenticationAuthority !== 'local'}
|
||||
title={user.authenticationAuthority !== 'local' ?
|
||||
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
|
||||
undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('settings.userinfo.fullname')}
|
||||
{user.authenticationAuthority !== 'local' && (
|
||||
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={userForm.fullName}
|
||||
onChange={(e) => handleUserFormChange('fullName', e.target.value)}
|
||||
placeholder={t('settings.userinfo.fullname')}
|
||||
readOnly={user.authenticationAuthority !== 'local'}
|
||||
title={user.authenticationAuthority !== 'local' ?
|
||||
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
|
||||
undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('settings.userinfo.email')}
|
||||
{user.authenticationAuthority !== 'local' && (
|
||||
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
className={styles.formInput}
|
||||
value={userForm.email}
|
||||
onChange={(e) => handleUserFormChange('email', e.target.value)}
|
||||
placeholder={t('settings.userinfo.email')}
|
||||
readOnly={user.authenticationAuthority !== 'local'}
|
||||
title={user.authenticationAuthority !== 'local' ?
|
||||
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
|
||||
undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('settings.userinfo.phone_name')}
|
||||
<span className={styles.fieldNote}>({t('settings.userinfo.phone_name.description')})</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={phoneName}
|
||||
onChange={(e) => handlePhoneNameChange(e.target.value)}
|
||||
placeholder={t('settings.userinfo.phone_name')}
|
||||
title="This field is saved locally and not sent to the server"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('settings.language')}
|
||||
<span className={styles.fieldNote}>({t('settings.language.description')})</span>
|
||||
</label>
|
||||
<select
|
||||
className={styles.formSelect}
|
||||
value={userForm.language}
|
||||
onChange={(e) => handleUserFormChange('language', e.target.value)}
|
||||
aria-label={t('settings.language')}
|
||||
>
|
||||
<option value="de">{getLanguageLabel('de')}</option>
|
||||
<option value="en">{getLanguageLabel('en')}</option>
|
||||
<option value="fr">{getLanguageLabel('fr')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
{/* Empty field to maintain layout */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>{t('settings.userinfo.privilege')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={userForm.privilege}
|
||||
readOnly
|
||||
placeholder={t('settings.userinfo.privilege')}
|
||||
title="This field is read-only"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>{t('settings.userinfo.auth_authority')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={user.authenticationAuthority}
|
||||
readOnly
|
||||
placeholder={t('settings.userinfo.auth_authority')}
|
||||
title="This field is read-only"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{updateMessage && (
|
||||
<div className={`${styles.updateMessage} ${updateMessage.type === 'success' ? styles.successMessage : styles.errorMessage}`}>
|
||||
{updateMessage.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.formActions}>
|
||||
<button
|
||||
className={styles.saveButton}
|
||||
onClick={handleSaveUserInfo}
|
||||
disabled={updateLoading}
|
||||
>
|
||||
{updateLoading ? t('settings.userinfo.saving') : t('settings.userinfo.save')}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsUser;
|
||||
229
src/hooks/privilegeCheckers.ts
Normal file
229
src/hooks/privilegeCheckers.ts
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
import { PrivilegeChecker } from '../components/PageManager/pageConfigInterface';
|
||||
|
||||
// Function to get current user privilege from localStorage (where it's cached)
|
||||
const getCurrentUserPrivilege = (): string | null => {
|
||||
try {
|
||||
const userData = localStorage.getItem('currentUser');
|
||||
console.log('🔍 Raw user data from localStorage:', userData);
|
||||
|
||||
if (userData) {
|
||||
const user = JSON.parse(userData);
|
||||
console.log('🔍 Parsed user object:', user);
|
||||
console.log('🔍 User privilege:', user.privilege);
|
||||
return user.privilege || null;
|
||||
}
|
||||
console.log('❌ No user data found in localStorage');
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error getting user privilege from localStorage:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Generic privilege checker for localStorage-based data with expiration
|
||||
export const createLocalStoragePrivilegeChecker = (
|
||||
dataKey: string,
|
||||
timestampKey: string,
|
||||
expirationHours: number = 24
|
||||
): PrivilegeChecker => {
|
||||
return (): boolean => {
|
||||
try {
|
||||
const savedData = localStorage.getItem(dataKey);
|
||||
const timestamp = localStorage.getItem(timestampKey);
|
||||
|
||||
console.log(`🔍 Checking privilege for ${dataKey}:`, {
|
||||
savedData: !!savedData,
|
||||
timestamp
|
||||
});
|
||||
|
||||
if (savedData && timestamp) {
|
||||
const dataTime = parseInt(timestamp);
|
||||
const now = Date.now();
|
||||
const hoursDiff = (now - dataTime) / (1000 * 60 * 60);
|
||||
|
||||
console.log(`📊 Privilege validation for ${dataKey}:`, {
|
||||
dataTime,
|
||||
now,
|
||||
hoursDiff,
|
||||
isValid: hoursDiff < expirationHours
|
||||
});
|
||||
|
||||
return hoursDiff < expirationHours;
|
||||
}
|
||||
|
||||
console.log(`❌ No privilege data found for ${dataKey}`);
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(`Error checking privilege for ${dataKey}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Generic privilege checker for user roles/permissions
|
||||
export const createRolePrivilegeChecker = (
|
||||
requiredRoles: string[],
|
||||
getUserRoles: () => string[] | Promise<string[]>
|
||||
): PrivilegeChecker => {
|
||||
return async (): Promise<boolean> => {
|
||||
try {
|
||||
const userRoles = await getUserRoles();
|
||||
const hasRequiredRole = requiredRoles.some(role => userRoles.includes(role));
|
||||
|
||||
console.log(`🔍 Checking role privilege:`, {
|
||||
requiredRoles,
|
||||
userRoles,
|
||||
hasRequiredRole
|
||||
});
|
||||
|
||||
return hasRequiredRole;
|
||||
} catch (error) {
|
||||
console.error('Error checking role privilege:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Generic privilege checker for feature flags
|
||||
export const createFeatureFlagChecker = (
|
||||
featureFlag: string,
|
||||
getFeatureFlags: () => Record<string, boolean> | Promise<Record<string, boolean>>
|
||||
): PrivilegeChecker => {
|
||||
return async (): Promise<boolean> => {
|
||||
try {
|
||||
const flags = await getFeatureFlags();
|
||||
const isEnabled = flags[featureFlag] === true;
|
||||
|
||||
console.log(`🔍 Checking feature flag ${featureFlag}:`, {
|
||||
isEnabled,
|
||||
allFlags: flags
|
||||
});
|
||||
|
||||
return isEnabled;
|
||||
} catch (error) {
|
||||
console.error(`Error checking feature flag ${featureFlag}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Generic privilege checker for authentication status
|
||||
export const createAuthPrivilegeChecker = (
|
||||
isAuthenticated: () => boolean | Promise<boolean>
|
||||
): PrivilegeChecker => {
|
||||
return async (): Promise<boolean> => {
|
||||
try {
|
||||
const authenticated = await isAuthenticated();
|
||||
console.log(`🔍 Checking authentication status:`, { authenticated });
|
||||
return authenticated;
|
||||
} catch (error) {
|
||||
console.error('Error checking authentication status:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to create custom privilege checkers
|
||||
export const createCustomPrivilegeChecker = (
|
||||
checkFunction: () => boolean | Promise<boolean>
|
||||
): PrivilegeChecker => {
|
||||
return checkFunction;
|
||||
};
|
||||
|
||||
// Predefined privilege checkers for common use cases
|
||||
export const privilegeCheckers = {
|
||||
// Speech signup checker (existing functionality)
|
||||
speechSignup: createLocalStoragePrivilegeChecker(
|
||||
'speechSignUpData',
|
||||
'speechSignUpTimestamp',
|
||||
24
|
||||
),
|
||||
|
||||
// Admin role checker - for admin and sysadmin users
|
||||
adminRole: createRolePrivilegeChecker(
|
||||
['admin', 'sysadmin'],
|
||||
() => {
|
||||
const userPrivilege = getCurrentUserPrivilege();
|
||||
console.log('🔍 Admin role check - user privilege:', userPrivilege);
|
||||
console.log('🔍 Admin role check - required roles: [admin, sysadmin]');
|
||||
console.log('🔍 Admin role check - user roles array:', userPrivilege ? [userPrivilege] : []);
|
||||
return Promise.resolve(userPrivilege ? [userPrivilege] : []);
|
||||
}
|
||||
),
|
||||
|
||||
// Sysadmin role checker - for sysadmin only
|
||||
sysadminRole: createRolePrivilegeChecker(
|
||||
['sysadmin'],
|
||||
() => {
|
||||
const userPrivilege = getCurrentUserPrivilege();
|
||||
return Promise.resolve(userPrivilege ? [userPrivilege] : []);
|
||||
}
|
||||
),
|
||||
|
||||
// Premium user checker
|
||||
premiumUser: createLocalStoragePrivilegeChecker(
|
||||
'premiumUserData',
|
||||
'premiumUserTimestamp',
|
||||
24 * 30 // 30 days
|
||||
),
|
||||
|
||||
// Feature flag checker
|
||||
betaFeatures: createFeatureFlagChecker(
|
||||
'betaFeatures',
|
||||
() => {
|
||||
// This would typically come from your feature flag service
|
||||
const flags = JSON.parse(localStorage.getItem('featureFlags') || '{}');
|
||||
return Promise.resolve(flags);
|
||||
}
|
||||
),
|
||||
|
||||
// Authentication checker
|
||||
authenticated: createAuthPrivilegeChecker(
|
||||
() => {
|
||||
// This would typically come from your auth context
|
||||
const token = localStorage.getItem('authToken');
|
||||
return Promise.resolve(!!token);
|
||||
}
|
||||
),
|
||||
|
||||
// User role checker - for user, admin, and sysadmin access
|
||||
userRole: createRolePrivilegeChecker(
|
||||
['user', 'admin', 'sysadmin'],
|
||||
() => {
|
||||
const userPrivilege = getCurrentUserPrivilege();
|
||||
return Promise.resolve(userPrivilege ? [userPrivilege] : []);
|
||||
}
|
||||
),
|
||||
|
||||
// Viewer role checker - for viewer, user, admin, and sysadmin access (all levels)
|
||||
viewerRole: createRolePrivilegeChecker(
|
||||
['viewer', 'user', 'admin', 'sysadmin'],
|
||||
() => {
|
||||
const userPrivilege = getCurrentUserPrivilege();
|
||||
console.log('🔍 Viewer role check - user privilege:', userPrivilege);
|
||||
console.log('🔍 Viewer role check - required roles: [viewer, user, admin, sysadmin]');
|
||||
console.log('🔍 Viewer role check - user roles array:', userPrivilege ? [userPrivilege] : []);
|
||||
return Promise.resolve(userPrivilege ? [userPrivilege] : []);
|
||||
}
|
||||
),
|
||||
|
||||
// Subscription checker - for paid features
|
||||
hasSubscription: createLocalStoragePrivilegeChecker(
|
||||
'subscriptionData',
|
||||
'subscriptionTimestamp',
|
||||
24 * 7 // 7 days
|
||||
),
|
||||
|
||||
// Mandate checker - for users who have submitted their mandate
|
||||
hasMandate: createLocalStoragePrivilegeChecker(
|
||||
'mandateData',
|
||||
'mandateTimestamp',
|
||||
24 * 30 // 30 days
|
||||
),
|
||||
|
||||
// Always allow access (for public pages)
|
||||
alwaysAllow: createCustomPrivilegeChecker(() => true),
|
||||
|
||||
// Never allow access (for disabled features)
|
||||
neverAllow: createCustomPrivilegeChecker(() => false)
|
||||
};
|
||||
114
src/hooks/privilegeTestUtils.ts
Normal file
114
src/hooks/privilegeTestUtils.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// Utility functions for testing and debugging the privilege system
|
||||
|
||||
import { privilegeCheckers } from './privilegeCheckers';
|
||||
|
||||
// Function to test all privilege checkers
|
||||
export const testAllPrivilegeCheckers = async () => {
|
||||
console.log('🧪 Testing all privilege checkers...');
|
||||
|
||||
const results = {
|
||||
speechSignup: await privilegeCheckers.speechSignup(),
|
||||
adminRole: await privilegeCheckers.adminRole(),
|
||||
sysadminRole: await privilegeCheckers.sysadminRole(),
|
||||
userRole: await privilegeCheckers.userRole(),
|
||||
viewerRole: await privilegeCheckers.viewerRole(),
|
||||
authenticated: await privilegeCheckers.authenticated(),
|
||||
alwaysAllow: await privilegeCheckers.alwaysAllow(),
|
||||
neverAllow: await privilegeCheckers.neverAllow()
|
||||
};
|
||||
|
||||
console.log('📊 Privilege checker results:', results);
|
||||
return results;
|
||||
};
|
||||
|
||||
// Function to simulate different user privilege levels for testing
|
||||
export const simulateUserPrivilege = (privilege: string) => {
|
||||
const testUser = {
|
||||
id: 'test-user',
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
fullName: 'Test User',
|
||||
language: 'en',
|
||||
enabled: true,
|
||||
privilege: privilege,
|
||||
authenticationAuthority: 'local',
|
||||
mandateId: 'test-mandate'
|
||||
};
|
||||
|
||||
localStorage.setItem('currentUser', JSON.stringify(testUser));
|
||||
console.log(`🎭 Simulated user with privilege: ${privilege}`);
|
||||
};
|
||||
|
||||
// Function to clear test data
|
||||
export const clearTestData = () => {
|
||||
localStorage.removeItem('currentUser');
|
||||
localStorage.removeItem('speechSignUpData');
|
||||
localStorage.removeItem('speechSignUpTimestamp');
|
||||
console.log('🧹 Cleared test data');
|
||||
};
|
||||
|
||||
// Function to get current user privilege for debugging
|
||||
export const getCurrentUserPrivilege = (): string | null => {
|
||||
try {
|
||||
const userData = localStorage.getItem('currentUser');
|
||||
if (userData) {
|
||||
const user = JSON.parse(userData);
|
||||
return user.privilege || null;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error getting user privilege:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Function to check if a specific page should be visible
|
||||
export const checkPageVisibility = async (pagePath: string) => {
|
||||
const privilegeMap: Record<string, any> = {
|
||||
'dashboard': privilegeCheckers.viewerRole,
|
||||
'dateien': privilegeCheckers.viewerRole,
|
||||
'prompts': privilegeCheckers.viewerRole,
|
||||
'team-bereich': privilegeCheckers.adminRole,
|
||||
'connections': privilegeCheckers.viewerRole,
|
||||
'workflows': privilegeCheckers.viewerRole,
|
||||
'einstellungen': privilegeCheckers.viewerRole,
|
||||
'speech': privilegeCheckers.viewerRole,
|
||||
'speech/transcripts': privilegeCheckers.speechSignup
|
||||
};
|
||||
|
||||
const checker = privilegeMap[pagePath];
|
||||
if (!checker) {
|
||||
console.log(`❌ No privilege checker found for page: ${pagePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasAccess = await checker();
|
||||
console.log(`🔍 Page ${pagePath} access:`, hasAccess);
|
||||
return hasAccess;
|
||||
};
|
||||
|
||||
// Function to test all pages visibility
|
||||
export const testAllPagesVisibility = async () => {
|
||||
console.log('🧪 Testing all pages visibility...');
|
||||
|
||||
const pages = [
|
||||
'dashboard',
|
||||
'dateien',
|
||||
'prompts',
|
||||
'team-bereich',
|
||||
'connections',
|
||||
'workflows',
|
||||
'einstellungen',
|
||||
'speech',
|
||||
'speech/transcripts'
|
||||
];
|
||||
|
||||
const results: Record<string, boolean> = {};
|
||||
|
||||
for (const page of pages) {
|
||||
results[page] = await checkPageVisibility(page);
|
||||
}
|
||||
|
||||
console.log('📊 All pages visibility results:', results);
|
||||
return results;
|
||||
};
|
||||
|
|
@ -347,6 +347,349 @@ export function useRegister() {
|
|||
};
|
||||
}
|
||||
|
||||
// Google Authentication
|
||||
interface GoogleAuthResponse {
|
||||
accessToken: string;
|
||||
tokenType: string;
|
||||
user: {
|
||||
username: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
mandateId: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function useGoogleAuth() {
|
||||
const [googleError, setGoogleError] = useState<string | null>(null);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
|
||||
const loginWithGoogle = async (): Promise<GoogleAuthResponse> => {
|
||||
setIsGoogleLoading(true);
|
||||
setGoogleError(null);
|
||||
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
const backendUrl = getApiBaseUrl();
|
||||
const loginUrl = `${backendUrl}/api/google/login`;
|
||||
|
||||
console.log('🔐 Starting Google authentication...');
|
||||
console.log('🌐 Backend URL:', backendUrl);
|
||||
console.log('🔗 Login URL:', loginUrl);
|
||||
|
||||
// First, get the Google login URL from the backend using fetch to avoid CORS issues
|
||||
fetch(`${backendUrl}/api/google/login`, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
redirect: 'manual' // Don't follow redirects
|
||||
})
|
||||
.then(response => {
|
||||
console.log('📡 Backend response:', response);
|
||||
console.log('📊 Response status:', response.status);
|
||||
console.log('📊 Response type:', response.type);
|
||||
console.log('📊 Response headers:', response.headers);
|
||||
|
||||
// Check if it's a redirect response
|
||||
if (response.status === 0 || response.type === 'opaque') {
|
||||
// This might be a CORS issue, try to get the redirect URL from the response
|
||||
console.log('🔄 CORS/Redirect detected, trying to extract URL from response');
|
||||
|
||||
// Try to read the response as text to get the redirect URL
|
||||
return response.text().then(text => {
|
||||
console.log('📄 Response text:', text);
|
||||
|
||||
// Look for redirect URL in the response
|
||||
const urlMatch = text.match(/https:\/\/accounts\.google\.com\/o\/oauth2\/auth[^"'\s]*/);
|
||||
if (urlMatch) {
|
||||
return { login_url: urlMatch[0] };
|
||||
}
|
||||
|
||||
// If no URL found in text, try to construct it from the error
|
||||
throw new Error('Could not extract Google OAuth URL from response');
|
||||
});
|
||||
} else if (response.status >= 200 && response.status < 300) {
|
||||
// Normal JSON response
|
||||
return response.json();
|
||||
} else if (response.status >= 300 && response.status < 400) {
|
||||
// Redirect response
|
||||
const location = response.headers.get('location');
|
||||
console.log('🔄 Redirect location:', location);
|
||||
if (location) {
|
||||
return { login_url: location };
|
||||
}
|
||||
throw new Error('Redirect response without location header');
|
||||
} else {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
console.log('📄 Response data:', data);
|
||||
|
||||
if (data.login_url) {
|
||||
// Open popup with the Google login URL
|
||||
const popup = window.open(
|
||||
data.login_url,
|
||||
'google-login',
|
||||
'width=500,height=600,scrollbars=yes,resizable=yes,top=100,left=100'
|
||||
);
|
||||
|
||||
if (!popup) {
|
||||
const errorMsg = 'Popup was blocked by browser. Please allow popups for this site and try again.';
|
||||
console.error('❌ Popup blocked:', errorMsg);
|
||||
setGoogleError(errorMsg);
|
||||
setIsGoogleLoading(false);
|
||||
reject(new Error('Popup was blocked'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Popup opened successfully');
|
||||
|
||||
// Listen for messages from the popup
|
||||
const messageListener = (event: MessageEvent) => {
|
||||
console.log('📨 Received message from popup:', event.origin, event.data);
|
||||
|
||||
// Verify origin for security
|
||||
const apiUrl = new URL(backendUrl);
|
||||
if (event.origin !== apiUrl.origin) {
|
||||
console.warn('⚠️ Message from unauthorized origin:', event.origin, 'Expected:', apiUrl.origin);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.type === 'google_auth_success') {
|
||||
console.log('✅ Google authentication successful');
|
||||
// Store the auth data with normalized field names
|
||||
if (event.data.token_data) {
|
||||
const normalizedTokenData = {
|
||||
accessToken: event.data.token_data.tokenAccess, // Convert tokenAccess to accessToken
|
||||
tokenType: event.data.token_data.tokenType,
|
||||
userId: event.data.token_data.userId,
|
||||
expiresAt: event.data.token_data.expiresAt,
|
||||
createdAt: event.data.token_data.createdAt
|
||||
};
|
||||
localStorage.setItem('auth_data', JSON.stringify(normalizedTokenData));
|
||||
console.log('💾 Auth data stored in localStorage');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
window.removeEventListener('message', messageListener);
|
||||
popup.close();
|
||||
setIsGoogleLoading(false);
|
||||
|
||||
// Resolve with the token data
|
||||
resolve({
|
||||
accessToken: event.data.token_data.tokenAccess,
|
||||
tokenType: event.data.token_data.tokenType || 'bearer',
|
||||
user: {
|
||||
username: '', // Will be populated by the backend
|
||||
email: '',
|
||||
fullName: '',
|
||||
mandateId: 0
|
||||
}
|
||||
});
|
||||
} else if (event.data.type === 'google_connection_error') {
|
||||
console.error('❌ Google connection error:', event.data.error);
|
||||
// Handle error
|
||||
window.removeEventListener('message', messageListener);
|
||||
popup.close();
|
||||
setIsGoogleLoading(false);
|
||||
setGoogleError(event.data.error || 'Google authentication failed');
|
||||
reject(new Error(event.data.error || 'Google authentication failed'));
|
||||
}
|
||||
};
|
||||
|
||||
// Add message listener
|
||||
window.addEventListener('message', messageListener);
|
||||
|
||||
// Handle popup closing without completing auth
|
||||
let popupClosedManually = false;
|
||||
const checkClosed = setInterval(() => {
|
||||
if (popup.closed) {
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageListener);
|
||||
setIsGoogleLoading(false);
|
||||
|
||||
if (!popupClosedManually) {
|
||||
console.warn('⚠️ Popup was closed before authentication completed');
|
||||
setGoogleError('Authentication was cancelled - popup was closed before completing login');
|
||||
} else {
|
||||
console.log('ℹ️ Popup closed after successful authentication');
|
||||
}
|
||||
|
||||
if (!popupClosedManually) {
|
||||
reject(new Error('Authentication was cancelled'));
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Set a timeout to detect if popup doesn't load
|
||||
const loadTimeout = setTimeout(() => {
|
||||
if (!popup.closed) {
|
||||
console.warn('⚠️ Popup did not load within 60 seconds');
|
||||
popup.close();
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageListener);
|
||||
setIsGoogleLoading(false);
|
||||
setGoogleError('Authentication timeout - please check your internet connection and try again');
|
||||
reject(new Error('Authentication timeout'));
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
// Override popup.close to mark as manually closed
|
||||
const originalClose = popup.close;
|
||||
popup.close = function() {
|
||||
popupClosedManually = true;
|
||||
clearTimeout(loadTimeout);
|
||||
return originalClose.call(this);
|
||||
};
|
||||
} else {
|
||||
throw new Error('No login URL received from backend');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ Failed to get Google login URL:', error);
|
||||
console.log('🔄 Attempting fallback approach...');
|
||||
|
||||
// Fallback: Try to construct the Google OAuth URL directly
|
||||
// This is a temporary solution until the backend is fixed
|
||||
const fallbackGoogleUrl = `${backendUrl}/api/google/login`;
|
||||
console.log('🔄 Using fallback URL:', fallbackGoogleUrl);
|
||||
|
||||
// Open popup with the fallback URL (let the backend handle the redirect)
|
||||
const popup = window.open(
|
||||
fallbackGoogleUrl,
|
||||
'google-login',
|
||||
'width=500,height=600,scrollbars=yes,resizable=yes,top=100,left=100'
|
||||
);
|
||||
|
||||
if (!popup) {
|
||||
const errorMsg = 'Popup was blocked by browser. Please allow popups for this site and try again.';
|
||||
console.error('❌ Popup blocked:', errorMsg);
|
||||
setGoogleError(errorMsg);
|
||||
setIsGoogleLoading(false);
|
||||
reject(new Error('Popup was blocked'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Popup opened successfully with fallback URL');
|
||||
|
||||
// Listen for messages from the popup
|
||||
const messageListener = (event: MessageEvent) => {
|
||||
console.log('📨 Received message from popup:', event.origin, event.data);
|
||||
|
||||
// Verify origin for security
|
||||
const apiUrl = new URL(backendUrl);
|
||||
if (event.origin !== apiUrl.origin) {
|
||||
console.warn('⚠️ Message from unauthorized origin:', event.origin, 'Expected:', apiUrl.origin);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data.type === 'google_auth_success') {
|
||||
console.log('✅ Google authentication successful');
|
||||
// Store the auth data with normalized field names
|
||||
if (event.data.token_data) {
|
||||
const normalizedTokenData = {
|
||||
accessToken: event.data.token_data.tokenAccess, // Convert tokenAccess to accessToken
|
||||
tokenType: event.data.token_data.tokenType,
|
||||
userId: event.data.token_data.userId,
|
||||
expiresAt: event.data.token_data.expiresAt,
|
||||
createdAt: event.data.token_data.createdAt
|
||||
};
|
||||
localStorage.setItem('auth_data', JSON.stringify(normalizedTokenData));
|
||||
console.log('💾 Auth data stored in localStorage');
|
||||
}
|
||||
|
||||
// Clean up
|
||||
window.removeEventListener('message', messageListener);
|
||||
popup.close();
|
||||
setIsGoogleLoading(false);
|
||||
|
||||
// Resolve with the token data
|
||||
resolve({
|
||||
accessToken: event.data.token_data.tokenAccess,
|
||||
tokenType: event.data.token_data.tokenType || 'bearer',
|
||||
user: {
|
||||
username: '', // Will be populated by the backend
|
||||
email: '',
|
||||
fullName: '',
|
||||
mandateId: 0
|
||||
}
|
||||
});
|
||||
} else if (event.data.type === 'google_connection_error') {
|
||||
console.error('❌ Google connection error:', event.data.error);
|
||||
// Handle error
|
||||
window.removeEventListener('message', messageListener);
|
||||
popup.close();
|
||||
setIsGoogleLoading(false);
|
||||
setGoogleError(event.data.error || 'Google authentication failed');
|
||||
reject(new Error(event.data.error || 'Google authentication failed'));
|
||||
}
|
||||
};
|
||||
|
||||
// Add message listener
|
||||
window.addEventListener('message', messageListener);
|
||||
|
||||
// Handle popup closing without completing auth
|
||||
let popupClosedManually = false;
|
||||
const checkClosed = setInterval(() => {
|
||||
if (popup.closed) {
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageListener);
|
||||
setIsGoogleLoading(false);
|
||||
|
||||
if (!popupClosedManually) {
|
||||
console.warn('⚠️ Popup was closed before authentication completed');
|
||||
setGoogleError('Authentication was cancelled - popup was closed before completing login');
|
||||
} else {
|
||||
console.log('ℹ️ Popup closed after successful authentication');
|
||||
}
|
||||
|
||||
if (!popupClosedManually) {
|
||||
reject(new Error('Authentication was cancelled'));
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Set a timeout to detect if popup doesn't load
|
||||
const loadTimeout = setTimeout(() => {
|
||||
if (!popup.closed) {
|
||||
console.warn('⚠️ Popup did not load within 60 seconds');
|
||||
popup.close();
|
||||
clearInterval(checkClosed);
|
||||
window.removeEventListener('message', messageListener);
|
||||
setIsGoogleLoading(false);
|
||||
setGoogleError('Authentication timeout - please check your internet connection and try again');
|
||||
reject(new Error('Authentication timeout'));
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
// Override popup.close to mark as manually closed
|
||||
const originalClose = popup.close;
|
||||
popup.close = function() {
|
||||
popupClosedManually = true;
|
||||
clearTimeout(loadTimeout);
|
||||
return originalClose.call(this);
|
||||
};
|
||||
});
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('❌ Google authentication error:', error);
|
||||
setGoogleError(error.message || 'Google authentication failed');
|
||||
setIsGoogleLoading(false);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loginWithGoogle,
|
||||
error: googleError,
|
||||
isLoading: isGoogleLoading
|
||||
};
|
||||
}
|
||||
|
||||
// Microsoft Registration
|
||||
interface MsalRegisterData {
|
||||
username: string;
|
||||
|
|
|
|||
|
|
@ -161,6 +161,46 @@ export function useConnections() {
|
|||
}
|
||||
};
|
||||
|
||||
// Refresh Microsoft token
|
||||
const refreshMicrosoftToken = async (connectionId: string): Promise<Connection> => {
|
||||
try {
|
||||
const data = await request({
|
||||
url: `/api/connections/${connectionId}/refresh-microsoft-token`,
|
||||
method: 'post'
|
||||
});
|
||||
|
||||
// Update local state
|
||||
setConnections(prev =>
|
||||
prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn)
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error refreshing Microsoft token:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Refresh Google token
|
||||
const refreshGoogleToken = async (connectionId: string): Promise<Connection> => {
|
||||
try {
|
||||
const data = await request({
|
||||
url: `/api/connections/${connectionId}/refresh-google-token`,
|
||||
method: 'post'
|
||||
});
|
||||
|
||||
// Update local state
|
||||
setConnections(prev =>
|
||||
prev.map(conn => conn.id === connectionId ? { ...conn, ...data } : conn)
|
||||
);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error refreshing Google token:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
connections,
|
||||
fetchConnections,
|
||||
|
|
@ -169,6 +209,8 @@ export function useConnections() {
|
|||
connectService,
|
||||
disconnectService,
|
||||
deleteConnection,
|
||||
refreshMicrosoftToken,
|
||||
refreshGoogleToken,
|
||||
isLoading,
|
||||
error
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<SidebarItem[]>([]);
|
||||
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);
|
||||
|
||||
// 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)
|
||||
}));
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<User, 'id' | 'mandateId'> & { 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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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?',
|
||||
|
|
|
|||
|
|
@ -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}"?',
|
||||
|
|
|
|||
|
|
@ -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}" ?',
|
||||
|
|
|
|||
|
|
@ -1,60 +1,20 @@
|
|||
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<User | null>(null);
|
||||
const [userLoading, setUserLoading] = useState(false);
|
||||
const [userError, setUserError] = useState<string | null>(null);
|
||||
|
||||
// Form state for user info
|
||||
const [userForm, setUserForm] = useState({
|
||||
username: '',
|
||||
fullName: '',
|
||||
email: '',
|
||||
language: 'en' as Language,
|
||||
privilege: '',
|
||||
enabled: true
|
||||
});
|
||||
const [updateMessage, setUpdateMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
|
||||
const [isUpdating, setIsUpdating] = useState(false); // Flag to prevent form reset during update
|
||||
const hasLoadedUser = useRef(false);
|
||||
|
||||
// Speech integration state
|
||||
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 = () => {
|
||||
try {
|
||||
|
|
@ -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 (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={sharedStyles.pageCard}>
|
||||
<div style={{ padding: '2rem', textAlign: 'center' }}>
|
||||
{isLoading ? t('common.loading') : t('settings.userinfo.loading')}
|
||||
{t('common.loading')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -355,143 +197,7 @@ function Einstellungen() {
|
|||
|
||||
<div className={sharedStyles.contentArea}>
|
||||
{/* User Information Section */}
|
||||
|
||||
{userError && (
|
||||
<div className={styles.errorMessage}>
|
||||
{t('settings.userinfo.error')}: {typeof userError === 'string' ? userError : 'An error occurred'}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user && (
|
||||
<div className={styles.userInfoForm}>
|
||||
<span className={styles.settingLabel}>{t('settings.userinfo')}</span>
|
||||
<span className={styles.settingDescription}>
|
||||
{t('settings.userinfo.description')}
|
||||
</span>
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('settings.userinfo.username')}
|
||||
{user.authenticationAuthority !== 'local' && (
|
||||
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={userForm.username}
|
||||
onChange={(e) => handleUserFormChange('username', e.target.value)}
|
||||
placeholder={t('settings.userinfo.username')}
|
||||
readOnly={user.authenticationAuthority !== 'local'}
|
||||
title={user.authenticationAuthority !== 'local' ?
|
||||
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
|
||||
undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('settings.userinfo.fullname')}
|
||||
{user.authenticationAuthority !== 'local' && (
|
||||
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={userForm.fullName}
|
||||
onChange={(e) => handleUserFormChange('fullName', e.target.value)}
|
||||
placeholder={t('settings.userinfo.fullname')}
|
||||
readOnly={user.authenticationAuthority !== 'local'}
|
||||
title={user.authenticationAuthority !== 'local' ?
|
||||
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
|
||||
undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('settings.userinfo.email')}
|
||||
{user.authenticationAuthority !== 'local' && (
|
||||
<span className={styles.fieldNote}>({t('settings.userinfo.managed_by').replace('{provider}', user.authenticationAuthority)})</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
className={styles.formInput}
|
||||
value={userForm.email}
|
||||
onChange={(e) => handleUserFormChange('email', e.target.value)}
|
||||
placeholder={t('settings.userinfo.email')}
|
||||
readOnly={user.authenticationAuthority !== 'local'}
|
||||
title={user.authenticationAuthority !== 'local' ?
|
||||
t('settings.userinfo.managed_note').replace('{provider}', user.authenticationAuthority) :
|
||||
undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>
|
||||
{t('settings.language')}
|
||||
<span className={styles.fieldNote}>({t('settings.language.description')})</span>
|
||||
</label>
|
||||
<select
|
||||
className={styles.formSelect}
|
||||
value={userForm.language}
|
||||
onChange={(e) => handleUserFormChange('language', e.target.value)}
|
||||
aria-label={t('settings.language')}
|
||||
>
|
||||
<option value="de">{getLanguageLabel('de')}</option>
|
||||
<option value="en">{getLanguageLabel('en')}</option>
|
||||
<option value="fr">{getLanguageLabel('fr')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formRow}>
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>{t('settings.userinfo.privilege')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={userForm.privilege}
|
||||
readOnly
|
||||
placeholder={t('settings.userinfo.privilege')}
|
||||
title="This field is read-only"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formField}>
|
||||
<label className={styles.fieldLabel}>{t('settings.userinfo.auth_authority')}</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.formInput}
|
||||
value={user.authenticationAuthority}
|
||||
readOnly
|
||||
placeholder={t('settings.userinfo.auth_authority')}
|
||||
title="This field is read-only"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{updateMessage && (
|
||||
<div className={`${styles.updateMessage} ${updateMessage.type === 'success' ? styles.successMessage : styles.errorMessage}`}>
|
||||
{updateMessage.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.formActions}>
|
||||
<button
|
||||
className={styles.saveButton}
|
||||
onClick={handleSaveUserInfo}
|
||||
disabled={updateLoading}
|
||||
>
|
||||
{updateLoading ? t('settings.userinfo.saving') : t('settings.userinfo.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<SettingsUser />
|
||||
|
||||
{/* Theme Setting */}
|
||||
<div className={styles.settingItem}>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={sharedStyles.pageContainer}>
|
||||
<div className={sharedStyles.pageCard}>
|
||||
<div className={sharedStyles.pageHeader}>
|
||||
<h1 className={sharedStyles.pageTitle}>Team-Bereich</h1>
|
||||
</div>
|
||||
<div className={sharedStyles.pageContent}>
|
||||
<MitgliederTable />
|
||||
<div className={sharedStyles.pageHeader}>
|
||||
<h1 className={sharedStyles.pageTitle}>{t('nav.team', 'Team-Bereich')}</h1>
|
||||
<button
|
||||
className={sharedStyles.primaryButton}
|
||||
onClick={handleAddUser}
|
||||
aria-label={t('users.add.title', 'Add User')}
|
||||
>
|
||||
<span className={sharedStyles.buttonIcon}><IoMdAdd /></span>
|
||||
{t('users.add.button', 'Add User')}
|
||||
</button>
|
||||
</div>
|
||||
<div className={sharedStyles.horizontalDivider}></div>
|
||||
|
||||
<div className={sharedStyles.contentArea}>
|
||||
<MitgliederTable
|
||||
showAddUser={showAddUser}
|
||||
onAddUserClose={() => setShowAddUser(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
<div className={styles.loginSection}>
|
||||
<div className={styles.loginBox}>
|
||||
<div className={styles.loginForm}>
|
||||
{(loginError || msalError) && (
|
||||
<div className={styles.error}>{loginError || msalError}</div>
|
||||
{(loginError || msalError || googleError) && (
|
||||
<div className={styles.error}>{loginError || msalError || googleError}</div>
|
||||
)}
|
||||
<div className={styles.floatingLabelInput}>
|
||||
<input
|
||||
|
|
@ -153,12 +165,12 @@ function Login() {
|
|||
|
||||
<button
|
||||
className={`${styles.button} ${styles.googleButton}`}
|
||||
onClick={() => console.log("Google button clicked")}
|
||||
disabled={false}
|
||||
onClick={handleGoogleLogin}
|
||||
disabled={isGoogleLoading}
|
||||
>
|
||||
<div className={styles.buttonContent}>
|
||||
<FaGoogle />
|
||||
Mit Google anmelden
|
||||
{isGoogleLoading ? "Signing in..." : "Mit Google anmelden"}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue