diff --git a/src/App.tsx b/src/App.tsx index 091da5f..e10dc56 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,9 +3,11 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import Login from './pages/Login'; import Register from './pages/Register'; -import { AuthProvider } from './auth/auth-provider'; +import { AuthProvider } from './auth/authProvider'; import { ProtectedRoute } from './auth/ProtectedRoute'; import Home from './pages/Home'; +import Dateien from './pages/Dateien/Dateien'; +import Mitglieder from './pages/Mitglieder/Mitglieder'; function App() { return ( @@ -19,7 +21,10 @@ function App() { - }> + }> + } /> + } /> + diff --git a/src/auth/auth-config.ts b/src/auth/authConfig.ts similarity index 100% rename from src/auth/auth-config.ts rename to src/auth/authConfig.ts diff --git a/src/auth/auth-provider.tsx b/src/auth/authProvider.tsx similarity index 98% rename from src/auth/auth-provider.tsx rename to src/auth/authProvider.tsx index d8a99d7..21e9754 100644 --- a/src/auth/auth-provider.tsx +++ b/src/auth/authProvider.tsx @@ -4,7 +4,7 @@ import { PublicClientApplication, InteractionStatus } from "@azure/msal-browser"; - import { msalConfig } from "./auth-config"; + import { msalConfig } from "./authConfig"; import { MsalProvider } from "@azure/msal-react"; import { ReactNode, useEffect, useState } from "react"; diff --git a/src/components/Dateien/DateienItem.module.css b/src/components/Dateien/DateienItem.module.css new file mode 100644 index 0000000..2e69493 --- /dev/null +++ b/src/components/Dateien/DateienItem.module.css @@ -0,0 +1,153 @@ +.fileItem { + display: flex; + align-items: center; + height: 60px; + padding: 0px 16px; + justify-content: space-between; + color: var(--Grayscale-Black, #24262B); + transition: background-color 0.2s ease; +} + +.fileItem:hover { + background-color: #f9f9f9; +} + +/* Column layout matching the header structure */ +.fileName { + flex: 3; + display: flex; + align-items: center; + overflow: hidden; + font-weight: 500; + color: #333; + padding-right: 8px; +} + +.fileName span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-left: 12px; +} + +.fileType { + flex: 1; + font-size: 14px; + color: #666; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.fileSize { + flex: 1; + font-size: 14px; + color: #666; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.fileDateWithActions { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.fileDate { + flex: 1.5; + font-size: 14px; + color: #666; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.icon { + font-size: 18px; + color: #757575; + flex-shrink: 0; +} + +.actionButtons { + flex: 1; + display: flex; + gap: 4px; + justify-content: flex-end; +} + +.downloadButton, +.deleteButton { + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 6px; + border: none; + border-radius: 4px; + background-color: transparent; + color: var(--text-color-secondary, #666); + cursor: pointer; + transition: all 0.2s ease; + min-width: 32px; +} + +.downloadButton:hover:not(:disabled), +.deleteButton:hover:not(:disabled) { + background-color: var(--background-color-hover, #e8e8e8); + color: var(--text-color-primary, #333); +} + +.deleteButton:hover:not(:disabled) { + color: #dc2626; +} + +.deleteButton.confirm { + background-color: #fee2e2; + color: #dc2626; +} + +.deleteButton.confirm:hover:not(:disabled) { + background-color: #fecaca; +} + +.downloadButton:disabled, +.deleteButton:disabled { + cursor: not-allowed; + opacity: 0.7; +} + +.actionIcon { + font-size: 16px; + flex-shrink: 0; +} + +.downloadButton.downloading, +.deleteButton.deleting { + background-color: var(--background-color-light, #f5f5f5); +} + +.actionText { + font-size: 12px; + color: var(--text-color-secondary, #666); + animation: pulse 1.5s infinite; + white-space: nowrap; +} + +.deleteButton.confirm .actionText { + color: #dc2626; + animation: none; +} + +@keyframes pulse { + 0% { + opacity: 0.6; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0.6; + } +} diff --git a/src/components/Dateien/DateienItem.tsx b/src/components/Dateien/DateienItem.tsx new file mode 100644 index 0000000..ee2d293 --- /dev/null +++ b/src/components/Dateien/DateienItem.tsx @@ -0,0 +1,127 @@ +import { FaFile, FaDownload, FaTrash } from "react-icons/fa"; +import styles from "./DateienItem.module.css"; +import { useState } from "react"; +import { useFileOperations } from "../../hooks/useFiles"; + +type DateienItemProps = { + file: { + id: number; + file_name: string; + action: string; + created_at: string; + size?: number; + }; + onDelete?: () => void; +}; + +/** + * Formats a file size in bytes to a human-readable string (KB, MB, etc.) + */ +const formatFileSize = (bytes?: number): string => { + if (bytes === undefined || bytes === null) return 'Unbekannte Größe'; + + if (bytes === 0) return '0 Bytes'; + + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + + if (i === 0) return `${bytes} ${sizes[i]}`; + + return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; +}; + +const DateienItem = ({ file, onDelete }: DateienItemProps) => { + const { downloadingFiles, deletingFiles, handleFileDownload, handleFileDelete } = useFileOperations(); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const isDownloading = downloadingFiles.has(file.id); + const isDeleting = deletingFiles.has(file.id); + + // Format the date properly + const formatDate = (dateString: string) => { + try { + const date = new Date(dateString); + // Check if date is valid + if (isNaN(date.getTime())) { + return 'Unbekanntes Datum'; + } + + return date.toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); + } catch (e) { + console.error('Error formatting date:', e); + return 'Unbekanntes Datum'; + } + }; + + const handleDeleteClick = async () => { + if (showDeleteConfirm) { + const success = await handleFileDelete(file.id); + if (success && onDelete) { + onDelete(); + } + setShowDeleteConfirm(false); + } else { + setShowDeleteConfirm(true); + } + }; + + const handleCancelDelete = () => { + setShowDeleteConfirm(false); + }; + + return ( +
  • + {/* 1st column: Name with icon */} +
    + + {file.file_name} +
    + + {/* 2nd column: Type */} +
    + {file.action} +
    + + {/* 3rd column: Size */} +
    + {formatFileSize(file.size)} +
    + + {/* 4th column: Date and action buttons */} +
    + + {formatDate(file.created_at)} + + +
    + + +
    +
    +
  • + ); +}; + +export default DateienItem; + diff --git a/src/components/Mitglieder/MitgliederItem.module.css b/src/components/Mitglieder/MitgliederItem.module.css new file mode 100644 index 0000000..ecc7396 --- /dev/null +++ b/src/components/Mitglieder/MitgliederItem.module.css @@ -0,0 +1,53 @@ +.memberItem { + display: flex; + align-items: center; + height: 70px; + padding: 0px 16px; + justify-content: space-between; + color: var(--Grayscale-Black, #24262B); + } + + .userProfile { + margin-right: 12px; + } + + .profileIcon { + font-size: 36px; + color: var(--Grayscale-Gray, #E9E9E9); + } + + .userInfo { + display: grid; + grid-template-columns: 200px 250px 100px; + gap: 16px; + flex-grow: 1; + align-items: center; + } + + .userName { + margin: 0; + font-size: 14px; + } + + .userEmail, + .userRole { + margin: 0; + font-size: 12px; + font-weight: light; + } + + .actions { + display: flex; + + align-items: center; + justify-content: center; + } + + .editBtn:hover { + color: var(--Brand-Green-Green, #3A8088); + } + + .deleteBtn:hover { + color: var(--Brand-Green-Green, #3A8088); + } + \ No newline at end of file diff --git a/src/components/Mitglieder/MitgliederItem.tsx b/src/components/Mitglieder/MitgliederItem.tsx new file mode 100644 index 0000000..b41d6aa --- /dev/null +++ b/src/components/Mitglieder/MitgliederItem.tsx @@ -0,0 +1,32 @@ +import { FaUserCircle } from "react-icons/fa"; +import styles from "./MitgliederItem.module.css"; +import MitgliederItemDelete from "./MitgliederItemDelete/MitgliederItemDelete"; +import MitgliederItemEdit from "./MitgliederItemEdit/MitgliederItemEdit"; +import { User } from "../../hooks/useUsers"; + +type MitgliederItemProps = { + user: User; + refetchUsers: () => Promise; + totalUsers: number; + }; + + const MitgliederItem = ({ user, refetchUsers, totalUsers }: MitgliederItemProps) => { + return ( +
  • +
    + +
    +
    + {user.fullName || user.username} + {user.email} + {user.privilege} +
    +
    + + +
    +
  • + ); + }; + + export default MitgliederItem; \ No newline at end of file diff --git a/src/components/Mitglieder/MitgliederItemDelete/DeletePopUp.module.css b/src/components/Mitglieder/MitgliederItemDelete/DeletePopUp.module.css new file mode 100644 index 0000000..b529250 --- /dev/null +++ b/src/components/Mitglieder/MitgliederItemDelete/DeletePopUp.module.css @@ -0,0 +1,92 @@ +.popupHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-top:10px; + margin-left: 10px; + margin-right: 10px; + margin-bottom: 5px; + width: 300px; + height: 20px; + color: gray; + font-size: 10px; + +} + +.closeButton { + display: flex; + align-items: center; + background: none; + border: none; + cursor: pointer; + width: 20px; /* Increased button size */ + height: 20px; + line-height: 0; + padding: 0; + margin:0; + +} + +.closeIcon { + width: 100%; /* Make the icon fill the button's width */ + height: 100%; + padding: 0; + margin:0; + display: block; +} + +.horizontalLineLight { + width: 100%; + background-color: #F1F1F1; + height: 1px; +} + +.userInfo { + display: grid; + grid-column: 1; + padding: none; + margin: none; + align-items: center; + justify-content: center; /* Center horizontally */ +} + +.userInfoParagraph{ + display: flex; + color: gray; + font-size: 10px; + margin:0px; + justify-content: center; + gap: 10px; +} +.userInfo h2{ + display: flex; + justify-content: center; + margin: 0; + margin-top:10px; + justify-content: center; +} + +.submitButtonDiv { + margin: 10px; + display: flex; /* Use Flexbox */ + justify-content: center; /* Center horizontally */ + align-items: center; +} +.submitButton { + background: none; + border: 1px solid black; + border-radius: 5px; + padding: 5px 20px; /* Optional: Adds some padding for better button size */ + cursor: pointer; + width: 80% +} + +.submitButton:hover { + background: rgb(168, 18, 18); + border: 1px solid rgb(168, 18, 18); + border-radius: 5px; + padding: 5px 20px; /* Optional: Adds some padding for better button size */ + cursor: pointer; + color: white; + transition: 0.2s; +} \ No newline at end of file diff --git a/src/components/Mitglieder/MitgliederItemDelete/DeletePopUp.tsx b/src/components/Mitglieder/MitgliederItemDelete/DeletePopUp.tsx new file mode 100644 index 0000000..925ad7d --- /dev/null +++ b/src/components/Mitglieder/MitgliederItemDelete/DeletePopUp.tsx @@ -0,0 +1,88 @@ +import { IoCloseOutline } from "react-icons/io5"; +import styles from "./DeletePopUp.module.css"; +import { User, useOrgUsers } from "../../../hooks/useUsers"; +import { useState } from "react"; + +type DeletePopUpProps = { + closePopup: () => void; + refetchUsers: () => Promise; + user: User; +}; + +const DeletePopUp = ({ closePopup, refetchUsers, user }: DeletePopUpProps) => { + const { deleteUser } = useOrgUsers(); + const [isDeleting, setIsDeleting] = useState(false); + const [error, setError] = useState(null); + + const handleDelete = async () => { + setIsDeleting(true); + setError(null); + + try { + await deleteUser(user.id); + // Refresh the users list and close the popup + await refetchUsers(); + closePopup(); + } catch (error: any) { + setError(error.message || "Failed to delete user"); + setIsDeleting(false); + } + }; + + return ( +
    +
    +
    +

    Delete User...

    + + +
    +
    + {error &&
    {error}
    } +
    +

    {user.fullName || user.username}

    +
    +

    Email: {user.email}

    +

    Privilege: {user.privilege}

    +
    +
    +
    +
    + +
    + +
    +
    + ) +} + +const popupStyles: { overlay: React.CSSProperties; popup: React.CSSProperties } = { + overlay: { + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0,0,0,0.5)", + display: "flex", + justifyContent: "center", + alignItems: "center", + + }, + popup: { + backgroundColor: "#fff", + borderRadius: "8px", + boxShadow: "0 5px 15px rgba(0,0,0,0.3)", + position: "relative" + } +}; + +export default DeletePopUp; \ No newline at end of file diff --git a/src/components/Mitglieder/MitgliederItemDelete/MitgliederItemDelete.module.css b/src/components/Mitglieder/MitgliederItemDelete/MitgliederItemDelete.module.css new file mode 100644 index 0000000..6a4c4bf --- /dev/null +++ b/src/components/Mitglieder/MitgliederItemDelete/MitgliederItemDelete.module.css @@ -0,0 +1,33 @@ +.deleteBtn { + background: none; + border: none; + cursor: pointer; + font-size: 18px; + color: #444; + height: 100; +} + +.deleteBtn:hover { + color: var(--Brand-Green-Green, #3A8088); +} + +.deleteBtn.disabled { + cursor: not-allowed; + opacity: 0.5; + background: none; + border: none; + cursor: pointer; + font-size: 18px; + color: #444; +} + +.deleteBtn.disabled:hover { + color: #444; +} + +.deleteBtnContainer { + display: flex; + align-items: center; + justify-content: center; + +} diff --git a/src/components/Mitglieder/MitgliederItemDelete/MitgliederItemDelete.tsx b/src/components/Mitglieder/MitgliederItemDelete/MitgliederItemDelete.tsx new file mode 100644 index 0000000..4bb686f --- /dev/null +++ b/src/components/Mitglieder/MitgliederItemDelete/MitgliederItemDelete.tsx @@ -0,0 +1,37 @@ +import { FaTrash } from "react-icons/fa"; +import styles from "./MitgliederItemDelete.module.css"; +import { useState } from "react"; +import DeletePopUp from "./DeletePopUp"; +import { User } from "../../../hooks/useUsers"; + +type MitgliederItemDeleteProps = { + user: User; + refetchUsers: () => Promise; + isDisabled?: boolean; +}; + +const MitgliederItemDelete = ({ user, refetchUsers, isDisabled = false }: MitgliederItemDeleteProps) => { + const [deleteWindow, setDeleteWindow] = useState(false); + + return ( +
    + + + {deleteWindow && ( + setDeleteWindow(false)} + refetchUsers={refetchUsers} + /> + )} +
    + ); + }; + + export default MitgliederItemDelete; \ No newline at end of file diff --git a/src/components/Mitglieder/MitgliederItemEdit/EditPopUp.module.css b/src/components/Mitglieder/MitgliederItemEdit/EditPopUp.module.css new file mode 100644 index 0000000..0faf35d --- /dev/null +++ b/src/components/Mitglieder/MitgliederItemEdit/EditPopUp.module.css @@ -0,0 +1,89 @@ +.popupHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; +} + +.popupHeader p { + font-size: 1.2rem; + font-weight: 600; + margin: 0; +} + +.closeButton { + background: none; + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.closeIcon { + font-size: 1.5rem; + color: #666; +} + +.horizontalLineLight { + height: 1px; + background-color: #e0e0e0; + width: 100%; + margin: 0; +} + +.form { + box-sizing: border-box; + max-width: 100%; + padding: 20px; +} + +.formGroup { + margin-bottom: 15px; + box-sizing: border-box; +} + +.formGroup label { + display: block; + margin-bottom: 5px; + font-weight: 500; +} + +.formGroup input, +.formGroup select { + width: 100%; + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 1rem; + box-sizing: border-box; +} + +.submitButtonDiv { + display: flex; + justify-content: flex-end; + padding: 15px 0 10px; +} + +.submitButton { + background-color: #4c9aff; + color: white; + border: none; + border-radius: 4px; + padding: 8px 16px; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.2s; +} + +.submitButton:hover { + background-color: #3d8df5; +} + +.userInfo h2 { + margin-top: 0; + margin-bottom: 10px; +} + + + diff --git a/src/components/Mitglieder/MitgliederItemEdit/EditPopUp.tsx b/src/components/Mitglieder/MitgliederItemEdit/EditPopUp.tsx new file mode 100644 index 0000000..2041f95 --- /dev/null +++ b/src/components/Mitglieder/MitgliederItemEdit/EditPopUp.tsx @@ -0,0 +1,152 @@ +import { IoCloseOutline } from "react-icons/io5"; +import { useState } from "react"; +import styles from "./EditPopUp.module.css"; +import { User, useOrgUsers } from "../../../hooks/useUsers"; + +type EditPopUpProps = { + closePopup: () => void; + refetchUsers: () => Promise; + user: User; +}; + +const EditPopUp = ({ closePopup, refetchUsers, user }: EditPopUpProps) => { + const { updateUser } = useOrgUsers(); + const [formData, setFormData] = useState({ + fullName: user.fullName || "", + email: user.email || "", + privilege: user.privilege || "", + username: user.username || "" + }); + const [isSubmitting, setIsSubmitting] = useState(false); + const [error, setError] = useState(null); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsSubmitting(true); + setError(null); + + try { + await updateUser(user.id, { + fullName: formData.fullName, + email: formData.email, + privilege: formData.privilege, + username: formData.username + }); + + // Refresh the users list and close the popup + await refetchUsers(); + closePopup(); + } catch (error: any) { + setError(error.message || "Failed to update user"); + } finally { + setIsSubmitting(false); + } + }; + + return ( +
    +
    +
    +

    Edit User

    + +
    +
    + {error &&
    {error}
    } +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    +
    +
    + ); +}; + +const popupStyles: { overlay: React.CSSProperties; popup: React.CSSProperties } = { + overlay: { + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0,0,0,0.5)", + display: "flex", + justifyContent: "center", + alignItems: "center", + + }, + popup: { + backgroundColor: "#fff", + borderRadius: "8px", + boxShadow: "0 5px 15px rgba(0,0,0,0.3)", + position: "relative", + width: "400px", + maxWidth: "90%" + } +}; + +export default EditPopUp; \ No newline at end of file diff --git a/src/components/Mitglieder/MitgliederItemEdit/MitgliederItemEdit.module.css b/src/components/Mitglieder/MitgliederItemEdit/MitgliederItemEdit.module.css new file mode 100644 index 0000000..7c91679 --- /dev/null +++ b/src/components/Mitglieder/MitgliederItemEdit/MitgliederItemEdit.module.css @@ -0,0 +1,12 @@ +.editBtn { + background: none; + border: none; + cursor: pointer; + font-size: 18px; + color: #444; + height: 100; +} + +.editBtn:hover { + color: #3d8df5; +} \ No newline at end of file diff --git a/src/components/Mitglieder/MitgliederItemEdit/MitgliederItemEdit.tsx b/src/components/Mitglieder/MitgliederItemEdit/MitgliederItemEdit.tsx new file mode 100644 index 0000000..aa21877 --- /dev/null +++ b/src/components/Mitglieder/MitgliederItemEdit/MitgliederItemEdit.tsx @@ -0,0 +1,36 @@ +import { FaEdit } from "react-icons/fa"; +import { useState } from "react"; +import styles from "./MitgliederItemEdit.module.css"; +import EditPopUp from "./EditPopUp"; +import { User } from "../../../hooks/useUsers"; + +type MitgliederItemEditProps = { + user: User; + refetchUsers: () => Promise; +}; + +const MitgliederItemEdit = ({ user, refetchUsers }: MitgliederItemEditProps) => { + const [editWindow, setEditWindow] = useState(false); + + return ( +
    + + + {editWindow && ( + setEditWindow(false)} + refetchUsers={refetchUsers} + /> + )} +
    + ); +}; + +export default MitgliederItemEdit; \ No newline at end of file diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 93cdd37..dce007f 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -1,9 +1,9 @@ import React from 'react' import styles from './Sidebar.module.css' import SidebarItem from './SidebarItem'; -import useSidebarData from './sidebarData'; +import useSidebarData from './SidebarData'; import SidebarUser from './SidebarUser'; -import { useCurrentUser } from '../../hooks/useCurrentUser'; +import { useCurrentUser } from '../../hooks/useUsers'; interface SidebarItemType { id: string; diff --git a/src/components/Sidebar/sidebarData.tsx b/src/components/Sidebar/sidebarData.tsx index d7f3dde..2c399a3 100644 --- a/src/components/Sidebar/sidebarData.tsx +++ b/src/components/Sidebar/sidebarData.tsx @@ -50,19 +50,19 @@ export const useSidebarData = () => { icon: BiInfoSquare, }, { - id: '6', + id: '7', name: 'Logs', link: '', icon: TbLogs , }, { - id: '7', + id: '8', name: 'Settings', link: '', icon: GoGear, }, { - id: '8', + id: '9', name: 'Help', link: '', icon: BiInfoSquare, diff --git a/src/hooks/useApi.ts b/src/hooks/useApi.ts new file mode 100644 index 0000000..d083884 --- /dev/null +++ b/src/hooks/useApi.ts @@ -0,0 +1,62 @@ +import { useState } from 'react'; +import api from '../api'; + +// Generic API error handling +export function formatApiError(error: any, defaultMessage: string): string { + if (error.response) { + return error.response.data?.detail || error.response.data?.message || defaultMessage; + } else if (error.request) { + return 'Keine Antwort vom Server erhalten'; + } else { + return error.message || defaultMessage; + } +} + +// Type for API request options +export interface ApiRequestOptions { + url: string; + method: 'get' | 'post' | 'put' | 'delete'; + data?: T; + params?: Record; + additionalConfig?: Record; // For responseType, headers, etc. +} + +// Hook for making API requests with consistent error handling +export function useApiRequest() { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const request = async ({ + url, + method, + data, + params, + additionalConfig = {} + }: ApiRequestOptions): Promise => { + setIsLoading(true); + setError(null); + + try { + const response = await api({ + url, + method, + data, + params, + ...additionalConfig + }); + return response.data; + } catch (error: any) { + const errorMessage = formatApiError(error, `Fehler bei ${method.toUpperCase()} ${url}`); + setError(errorMessage); + throw new Error(errorMessage); + } finally { + setIsLoading(false); + } + }; + + return { + request, + isLoading, + error + }; +} \ No newline at end of file diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts deleted file mode 100644 index 35f312c..0000000 --- a/src/hooks/useAuth.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { useState } from 'react'; -import axios from 'axios'; - -interface LoginResponse { - accessToken: string; - tokenType: string; - label?: any; - fieldLabels?: any; -} - -interface UseAuthReturn { - login: (username: string, password: string) => Promise; - error: string | null; - isLoading: boolean; -} - -export function useAuth(): UseAuthReturn { - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const login = async (username: string, password: string): Promise => { - setIsLoading(true); - setError(null); - - try { - // Create the form data in the exact format FastAPI expects - const params = new URLSearchParams(); - params.append('username', username); - params.append('password', password); - params.append('grant_type', 'password'); - params.append('scope', ''); - params.append('client_id', ''); - params.append('client_secret', ''); - - // Create a custom axios instance for this request - const instance = axios.create({ - baseURL: 'https://gateway.poweron-center.net', - withCredentials: true, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }); - - const response = await instance.post('/api/token', params); - - console.log('Login response:', response.data); - - // Store the entire auth response - if (response.data.accessToken) { - localStorage.setItem('auth_data', JSON.stringify(response.data)); - } - - return response.data; - } catch (error: any) { - let errorMessage = 'An error occurred during login'; - - if (error.response) { - // Log the complete error details including the request that was sent - console.error('Login error details:', { - status: error.response.status, - statusText: error.response.statusText, - data: error.response.data, - headers: error.response.headers, - request: { - url: error.config?.url, - method: error.config?.method, - data: error.config?.data, - params: error.config?.params - } - }); - errorMessage = error.response.data?.detail || 'Invalid username or password'; - } else if (error.request) { - console.error('No response received:', error.request); - errorMessage = 'No response received from server'; - } else { - console.error('Error:', error.message); - errorMessage = error.message; - } - - setError(errorMessage); - throw error; - } finally { - setIsLoading(false); - } - }; - - return { - login, - error, - isLoading - }; -} \ No newline at end of file diff --git a/src/hooks/useAuthentication.ts b/src/hooks/useAuthentication.ts new file mode 100644 index 0000000..b0094a6 --- /dev/null +++ b/src/hooks/useAuthentication.ts @@ -0,0 +1,290 @@ +import { useState } from 'react'; +import axios from 'axios'; +import { useMsal } from '@azure/msal-react'; +import api from '../api'; +import { useApiRequest } from './useApi'; + +// Regular authentication +interface LoginResponse { + accessToken: string; + tokenType: string; + label?: any; + fieldLabels?: any; +} + +export function useAuth() { + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const login = async (username: string, password: string): Promise => { + setIsLoading(true); + setError(null); + + try { + // Create the form data in the exact format FastAPI expects + const params = new URLSearchParams(); + params.append('username', username); + params.append('password', password); + params.append('grant_type', 'password'); + params.append('scope', ''); + params.append('client_id', ''); + params.append('client_secret', ''); + + // Create a custom axios instance for this request + const instance = axios.create({ + baseURL: 'https://gateway.poweron-center.net', + withCredentials: true, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }); + + const response = await instance.post('/api/token', params); + + // Store the entire auth response + if (response.data.accessToken) { + localStorage.setItem('auth_data', JSON.stringify(response.data)); + } + + return response.data; + } catch (error: any) { + let errorMessage = 'An error occurred during login'; + + if (error.response) { + errorMessage = error.response.data?.detail || 'Invalid username or password'; + } else if (error.request) { + errorMessage = 'No response received from server'; + } else { + errorMessage = error.message; + } + + setError(errorMessage); + throw error; + } finally { + setIsLoading(false); + } + }; + + return { + login, + error, + isLoading + }; +} + +// Microsoft Authentication +interface MsalAuthResponse { + accessToken: string; + tokenType: string; + user: { + username: string; + email: string; + fullName: string; + mandateId: number; + }; +} + +export function useMsalAuth() { + const { instance, accounts } = useMsal(); + const { request, isLoading, error } = useApiRequest(); + const [msalError, setMsalError] = useState(null); + const [isMsalLoading, setIsMsalLoading] = useState(false); + + const loginWithMsal = async (): Promise => { + setIsMsalLoading(true); + setMsalError(null); + + try { + let msalToken; + + // If we have an account, try to get the token silently + if (accounts.length > 0) { + const silentRequest = { + scopes: ['user.read'], + account: accounts[0] + }; + + try { + const response = await instance.acquireTokenSilent(silentRequest); + msalToken = response.accessToken; + } catch (e) { + // If silent token acquisition fails, fall back to popup + const response = await instance.acquireTokenPopup(silentRequest); + msalToken = response.accessToken; + } + } else { + // No account, do popup login + const response = await instance.loginPopup({ + scopes: ['user.read'] + }); + + if (response.account) { + const tokenResponse = await instance.acquireTokenSilent({ + scopes: ['user.read'], + account: response.account + }); + msalToken = tokenResponse.accessToken; + } else { + throw new Error('Failed to get account after login'); + } + } + + // Exchange MSAL token for backend token + const response = await api.post('/api/msft/token', null, { + headers: { + 'Authorization': `Bearer ${msalToken}` + } + }); + + // Store the backend token + if (response.data.accessToken) { + localStorage.setItem('auth_data', JSON.stringify(response.data)); + } + + return response.data; + } catch (error: any) { + let errorMessage = 'MSAL Login fehlgeschlagen'; + + if (error.response) { + errorMessage = error.response.data?.detail || error.response.data?.message || errorMessage; + } else if (error.request) { + errorMessage = 'Keine Antwort vom Server erhalten'; + } else { + errorMessage = error.message || errorMessage; + } + + setMsalError(errorMessage); + throw new Error(errorMessage); + } finally { + setIsMsalLoading(false); + } + }; + + return { + loginWithMsal, + error: msalError || error, + isLoading: isMsalLoading || isLoading + }; +} + +// Registration +interface RegisterData { + username: string; + password: string; + email: string; + fullName: string; + language?: string; +} + +interface RegisterResponse { + success: boolean; + message?: string; + user?: { + username: string; + email: string; + fullName: string; + }; +} + +export function useRegister() { + const { request, isLoading, error } = useApiRequest(); + + const register = async (userData: RegisterData): Promise => { + try { + // Add default language if not provided + const dataToSend = { + ...userData, + language: userData.language || 'de' + }; + + const response = await request({ + url: '/api/users/register', + method: 'post', + data: dataToSend, + additionalConfig: { + headers: { + 'Content-Type': 'application/json' + } + } + }); + + return { + success: true, + message: 'Registration successful', + user: response + }; + } catch (error: any) { + throw error; + } + }; + + return { + register, + error, + isLoading + }; +} + +// Microsoft Registration +interface MsalRegisterData { + username: string; + email: string; + fullName: string; + language?: string; +} + +export function useMsalRegister() { + const { instance, accounts } = useMsal(); + const { request, isLoading, error } = useApiRequest(); + + const registerWithMsal = async (): Promise => { + try { + if (!accounts || accounts.length === 0) { + // If not signed in with Microsoft, sign in first + await instance.loginPopup({ + scopes: ['user.read'] + }); + } + + // Get the current account + const currentAccount = instance.getAllAccounts()[0]; + if (!currentAccount) { + throw new Error('No Microsoft account found'); + } + + // Prepare user data from Microsoft account + const userData: MsalRegisterData = { + username: currentAccount.username, + email: currentAccount.username, + fullName: currentAccount.name || currentAccount.username, + language: 'de' + }; + + // Register the user through our backend + const response = await request({ + url: '/api/users/register-with-msal', + method: 'post', + data: userData, + additionalConfig: { + headers: { + 'Content-Type': 'application/json' + } + } + }); + + return { + success: true, + message: 'Registration successful', + user: response + }; + } catch (error: any) { + throw error; + } + }; + + return { + registerWithMsal, + error, + isLoading + }; +} \ No newline at end of file diff --git a/src/hooks/useCurrentUser.ts b/src/hooks/useCurrentUser.ts deleted file mode 100644 index 7d8c99d..0000000 --- a/src/hooks/useCurrentUser.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useState, useEffect } from 'react'; -import api from '../api'; - -interface CurrentUser { - id: number; - username: string; - fullName: string; - email: string; - privilege: string; - mandateId: number; -} - -interface UseCurrentUserReturn { - user: CurrentUser | null; - error: string | null; - isLoading: boolean; - refetch: () => Promise; -} - -export function useCurrentUser(): UseCurrentUserReturn { - const [user, setUser] = useState(null); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const fetchCurrentUser = async () => { - setIsLoading(true); - setError(null); - - try { - const response = await api.get('/api/user/me'); - setUser(response.data); - } catch (error: any) { - let errorMessage = 'Fehler beim Abrufen des Benutzerprofils'; - - if (error.response) { - errorMessage = error.response.data?.detail || error.response.data?.message || errorMessage; - } else if (error.request) { - errorMessage = 'Keine Antwort vom Server erhalten'; - } else { - errorMessage = error.message || errorMessage; - } - - setError(errorMessage); - setUser(null); - } finally { - setIsLoading(false); - } - }; - - useEffect(() => { - fetchCurrentUser(); - }, []); - - return { - user, - error, - isLoading, - refetch: fetchCurrentUser - }; -} \ No newline at end of file diff --git a/src/hooks/useFiles.ts b/src/hooks/useFiles.ts new file mode 100644 index 0000000..85d063a --- /dev/null +++ b/src/hooks/useFiles.ts @@ -0,0 +1,209 @@ +import { useState, useEffect } from 'react'; +import { useApiRequest } from './useApi'; + +// File interfaces +export interface FileInfo { + id: number; + name: string; + mimeType: string; + size?: number; + creationDate: string; + fileHash?: string; + mandateId?: number; + userId?: number; + workflowId?: string; +} + +export interface UserFile { + id: number; + file_name: string; + action: string; + created_at: string; + size?: number; +} + +// Files list hook +export function useUserFiles() { + const [files, setFiles] = useState([]); + const { request, isLoading: loading, error } = useApiRequest(); + + const fetchFiles = async () => { + try { + const data = await request({ + url: '/api/files', + method: 'get' + }); + + // Map API response to our frontend model + const mappedFiles = data.map((apiFile: FileInfo): UserFile => { + // Derive a simplified action from the MIME type + let action = 'Document'; + if (apiFile.mimeType) { + const mimePrefix = apiFile.mimeType.split('/')[0]; + switch (mimePrefix) { + case 'image': + action = 'Bild'; + break; + case 'application': + if (apiFile.mimeType.includes('pdf')) { + action = 'PDF'; + } else if (apiFile.mimeType.includes('word') || apiFile.mimeType.includes('office')) { + action = 'Dokument'; + } else if (apiFile.mimeType.includes('excel') || apiFile.mimeType.includes('spreadsheet')) { + action = 'Tabelle'; + } else { + action = 'Datei'; + } + break; + case 'text': + action = 'Text'; + break; + case 'video': + action = 'Video'; + break; + case 'audio': + action = 'Audio'; + break; + default: + action = 'Datei'; + } + } + + return { + id: apiFile.id, + file_name: apiFile.name, + action: action, + created_at: apiFile.creationDate, + size: apiFile.size + }; + }); + + setFiles(mappedFiles); + } catch (error) { + // Error is already handled by useApiRequest + } + }; + + useEffect(() => { + fetchFiles(); + }, []); + + return { files, loading, error, refetch: fetchFiles }; +} + +// File operations hook +export function useFileOperations() { + const [downloadingFiles, setDownloadingFiles] = useState>(new Set()); + const [deletingFiles, setDeletingFiles] = useState>(new Set()); + const [uploadingFile, setUploadingFile] = useState(false); + const { request, error: apiError, isLoading } = useApiRequest(); + const [downloadError, setDownloadError] = useState(null); + const [deleteError, setDeleteError] = useState(null); + const [uploadError, setUploadError] = useState(null); + + const handleFileDownload = async (fileId: number, fileName: string) => { + setDownloadError(null); + setDownloadingFiles(prev => new Set(prev).add(fileId)); + + try { + const blob = await request({ + url: `/api/files/${fileId}`, + method: 'get', + // Override axios config for blob response + additionalConfig: { responseType: 'blob' } + }); + + // Create a download link and trigger the download + const url = window.URL.createObjectURL(new Blob([blob])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', fileName); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + return true; + } catch (error: any) { + setDownloadError(error.message); + return false; + } finally { + setDownloadingFiles(prev => { + const newSet = new Set(prev); + newSet.delete(fileId); + return newSet; + }); + } + }; + + const handleFileDelete = async (fileId: number) => { + setDeleteError(null); + setDeletingFiles(prev => new Set(prev).add(fileId)); + + try { + await request({ + url: `/api/files/${fileId}`, + method: 'delete' + }); + + // Add a small delay to ensure backend has time to process + await new Promise(resolve => setTimeout(resolve, 300)); + return true; + } catch (error: any) { + setDeleteError(error.message); + return false; + } finally { + setDeletingFiles(prev => { + const newSet = new Set(prev); + newSet.delete(fileId); + return newSet; + }); + } + }; + + const handleFileUpload = async (file: globalThis.File, workflowId?: string) => { + setUploadError(null); + setUploadingFile(true); + + try { + const formData = new FormData(); + formData.append('file', file); + + if (workflowId) { + formData.append('workflowId', workflowId); + } + + const fileData = await request({ + url: '/api/files/upload', + method: 'post', + data: formData, + // Override axios config for form data + additionalConfig: { + headers: { + 'Content-Type': 'multipart/form-data', + } + } + }); + + return { success: true, fileData }; + } catch (error: any) { + setUploadError(error.message); + return { success: false, error: error.message }; + } finally { + setUploadingFile(false); + } + }; + + return { + downloadingFiles, + deletingFiles, + uploadingFile, + downloadError, + deleteError, + uploadError, + handleFileDownload, + handleFileDelete, + handleFileUpload, + isLoading + }; +} \ No newline at end of file diff --git a/src/hooks/useMsalAuth.ts b/src/hooks/useMsalAuth.ts deleted file mode 100644 index 69dfc5e..0000000 --- a/src/hooks/useMsalAuth.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { useState } from 'react'; -import { useMsal } from '@azure/msal-react'; -import api from '../api'; - -interface MsalAuthResponse { - accessToken: string; - tokenType: string; - user: { - username: string; - email: string; - fullName: string; - mandateId: number; - }; -} - -interface UseMsalAuthReturn { - loginWithMsal: () => Promise; - error: string | null; - isLoading: boolean; -} - -export function useMsalAuth(): UseMsalAuthReturn { - const { instance, accounts } = useMsal(); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const loginWithMsal = async (): Promise => { - setIsLoading(true); - setError(null); - - try { - let msalToken; - console.log("Starting MSAL auth process"); - - // If we have an account, try to get the token silently - if (accounts.length > 0) { - console.log("Account found, trying silent token acquisition"); - const silentRequest = { - scopes: ['user.read'], - account: accounts[0] - }; - - try { - const response = await instance.acquireTokenSilent(silentRequest); - msalToken = response.accessToken; - console.log("Silent token acquisition successful"); - } catch (e) { - // If silent token acquisition fails, fall back to popup - console.log("Silent token acquisition failed, trying popup", e); - const response = await instance.acquireTokenPopup(silentRequest); - msalToken = response.accessToken; - console.log("Popup token acquisition successful"); - } - } else { - // No account, do popup login - console.log("No account found, doing popup login"); - const response = await instance.loginPopup({ - scopes: ['user.read'] - }); - - if (response.account) { - console.log("Login successful, acquiring token"); - const tokenResponse = await instance.acquireTokenSilent({ - scopes: ['user.read'], - account: response.account - }); - msalToken = tokenResponse.accessToken; - console.log("Token acquisition successful"); - } else { - throw new Error('Failed to get account after login'); - } - } - - console.log("MSAL token acquired, exchanging with backend"); - - // Exchange MSAL token for backend token - const response = await api.post('/api/msft/token', null, { - headers: { - 'Authorization': `Bearer ${msalToken}` - } - }); - - console.log("Backend token exchange successful", response.data); - - // Store the backend token - if (response.data.accessToken) { - localStorage.setItem('auth_data', JSON.stringify(response.data)); - } - - return response.data; - } catch (error: any) { - let errorMessage = 'MSAL Login fehlgeschlagen'; - - if (error.response) { - console.error("Backend response error:", error.response.status, error.response.data); - errorMessage = error.response.data?.detail || error.response.data?.message || errorMessage; - } else if (error.request) { - console.error("No response received:", error.request); - errorMessage = 'Keine Antwort vom Server erhalten'; - } else { - console.error("Error during MSAL auth:", error.message); - errorMessage = error.message || errorMessage; - } - - setError(errorMessage); - throw new Error(errorMessage); - } finally { - setIsLoading(false); - } - }; - - return { - loginWithMsal, - error, - isLoading - }; -} \ No newline at end of file diff --git a/src/hooks/useMsalRegister.ts b/src/hooks/useMsalRegister.ts deleted file mode 100644 index 6f1a0ce..0000000 --- a/src/hooks/useMsalRegister.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { useState } from 'react'; -import { useMsal } from '@azure/msal-react'; -import api from '../api'; - -interface MsalRegisterData { - username: string; - email: string; - fullName: string; - language?: string; -} - -interface MsalRegisterResponse { - success: boolean; - message?: string; - user?: { - username: string; - email: string; - fullName: string; - }; -} - -interface UseMsalRegisterReturn { - registerWithMsal: () => Promise; - error: string | null; - isLoading: boolean; -} - -export function useMsalRegister(): UseMsalRegisterReturn { - const { instance, accounts } = useMsal(); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const registerWithMsal = async (): Promise => { - setIsLoading(true); - setError(null); - - try { - if (!accounts || accounts.length === 0) { - // If not signed in with Microsoft, sign in first - await instance.loginPopup({ - scopes: ['user.read'] - }); - } - - // Get the current account - const currentAccount = instance.getAllAccounts()[0]; - if (!currentAccount) { - throw new Error('No Microsoft account found'); - } - - // Prepare user data from Microsoft account - const userData: MsalRegisterData = { - username: currentAccount.username, - email: currentAccount.username, - fullName: currentAccount.name || currentAccount.username, - language: 'de' - }; - - // Register the user through our backend - const response = await api.post('/api/users/register-with-msal', userData, { - headers: { - 'Content-Type': 'application/json' - } - }); - - return { - success: true, - message: 'Registration successful', - user: response.data - }; - } catch (error: any) { - let errorMessage = 'MSAL Registrierung fehlgeschlagen'; - - if (error.response) { - errorMessage = error.response.data?.detail || error.response.data?.message || errorMessage; - } else if (error.request) { - errorMessage = 'Keine Antwort vom Server erhalten'; - } else { - errorMessage = error.message || errorMessage; - } - - setError(errorMessage); - throw new Error(errorMessage); - } finally { - setIsLoading(false); - } - }; - - return { - registerWithMsal, - error, - isLoading - }; -} \ No newline at end of file diff --git a/src/hooks/useRegister.ts b/src/hooks/useRegister.ts deleted file mode 100644 index 4417f00..0000000 --- a/src/hooks/useRegister.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { useState } from 'react'; -import api from '../api'; - -interface RegisterData { - username: string; - password: string; - email: string; - fullName: string; - language?: string; -} - -interface RegisterResponse { - success: boolean; - message?: string; - user?: { - username: string; - email: string; - fullName: string; - }; -} - -interface UseRegisterReturn { - register: (data: RegisterData) => Promise; - error: string | null; - isLoading: boolean; -} - -export function useRegister(): UseRegisterReturn { - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const register = async (userData: RegisterData): Promise => { - setIsLoading(true); - setError(null); - - try { - // Add default language if not provided - const dataToSend = { - ...userData, - language: userData.language || 'de' - }; - - const response = await api.post('/api/users/register', dataToSend, { - headers: { - 'Content-Type': 'application/json' - } - }); - - return { - success: true, - message: 'Registration successful', - user: response.data - }; - } catch (error: any) { - let errorMessage = 'Registrierung fehlgeschlagen'; - - // Handle different types of errors - if (error.response) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - errorMessage = error.response.data?.detail || error.response.data?.message || errorMessage; - } else if (error.request) { - // The request was made but no response was received - errorMessage = 'Keine Antwort vom Server erhalten'; - } else { - // Something happened in setting up the request - errorMessage = error.message || errorMessage; - } - - setError(errorMessage); - throw new Error(errorMessage); - } finally { - setIsLoading(false); - } - }; - - return { - register, - error, - isLoading - }; -} \ No newline at end of file diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts deleted file mode 100644 index aefd67d..0000000 --- a/src/hooks/useUser.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { useState } from 'react'; -import api from '../api'; - -interface User { - id: number; - username: string; - email: string; - fullName: string; - mandateId: number; -} - -interface UseUserReturn { - deleteUser: (userId: number) => Promise; - updateUser: (userId: number, userData: Partial) => Promise; - getUser: (userId: number) => Promise; - error: string | null; - isLoading: boolean; -} - -export function useUser(): UseUserReturn { - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(false); - - const deleteUser = async (userId: number): Promise => { - setIsLoading(true); - setError(null); - - try { - await api.delete(`/api/users/${userId}`); - } catch (error: any) { - let errorMessage = 'Fehler beim Löschen des Benutzers'; - - if (error.response) { - errorMessage = error.response.data?.detail || error.response.data?.message || errorMessage; - } else if (error.request) { - errorMessage = 'Keine Antwort vom Server erhalten'; - } else { - errorMessage = error.message || errorMessage; - } - - setError(errorMessage); - throw new Error(errorMessage); - } finally { - setIsLoading(false); - } - }; - - const updateUser = async (userId: number, userData: Partial): Promise => { - setIsLoading(true); - setError(null); - - try { - const response = await api.put(`/api/users/${userId}`, userData); - return response.data; - } catch (error: any) { - let errorMessage = 'Fehler beim Aktualisieren des Benutzers'; - - if (error.response) { - errorMessage = error.response.data?.detail || error.response.data?.message || errorMessage; - } else if (error.request) { - errorMessage = 'Keine Antwort vom Server erhalten'; - } else { - errorMessage = error.message || errorMessage; - } - - setError(errorMessage); - throw new Error(errorMessage); - } finally { - setIsLoading(false); - } - }; - - const getUser = async (userId: number): Promise => { - setIsLoading(true); - setError(null); - - try { - const response = await api.get(`/api/users/${userId}`); - return response.data; - } catch (error: any) { - let errorMessage = 'Fehler beim Abrufen des Benutzers'; - - if (error.response) { - errorMessage = error.response.data?.detail || error.response.data?.message || errorMessage; - } else if (error.request) { - errorMessage = 'Keine Antwort vom Server erhalten'; - } else { - errorMessage = error.message || errorMessage; - } - - setError(errorMessage); - throw new Error(errorMessage); - } finally { - setIsLoading(false); - } - }; - - return { - deleteUser, - updateUser, - getUser, - error, - isLoading - }; -} \ No newline at end of file diff --git a/src/hooks/useUsers.ts b/src/hooks/useUsers.ts new file mode 100644 index 0000000..70ae9fa --- /dev/null +++ b/src/hooks/useUsers.ts @@ -0,0 +1,134 @@ +import { useState, useEffect } from 'react'; +import { useApiRequest } from './useApi'; + +// User interfaces +export interface User { + id: number; + username: string; + email: string; + fullName: string; + privilege: string; + mandateId: number; +} + +export type UserUpdateData = Partial>; + +// Current user hook +export function useCurrentUser() { + const [user, setUser] = useState(null); + const { request, isLoading, error } = useApiRequest(); + + const fetchCurrentUser = async () => { + try { + const data = await request({ + url: '/api/user/me', + method: 'get' + }); + setUser(data); + } catch (error) { + setUser(null); + } + }; + + useEffect(() => { + fetchCurrentUser(); + }, []); + + return { + user, + error, + isLoading, + refetch: fetchCurrentUser + }; +} + +// Organization users hook (list, update, delete) +export function useOrgUsers() { + const [users, setUsers] = useState([]); + const { request, isLoading: loading, error } = useApiRequest(); + + const fetchUsers = async () => { + try { + const data = await request({ + url: '/api/users', + method: 'get' + }); + setUsers(data); + } catch (error) { + // Error is already handled by useApiRequest + } + }; + + const updateUser = async (userId: number, userData: UserUpdateData) => { + await request({ + url: `/api/users/${userId}`, + method: 'put', + data: userData + }); + await fetchUsers(); // Refresh the list after update + }; + + const deleteUser = async (userId: number) => { + await request({ + url: `/api/users/${userId}`, + method: 'delete' + }); + await fetchUsers(); // Refresh the list after deletion + }; + + const getUser = async (userId: number): Promise => { + return await request({ + url: `/api/users/${userId}`, + method: 'get' + }); + }; + + useEffect(() => { + fetchUsers(); + }, []); + + return { + users, + loading, + error, + refetch: fetchUsers, + updateUser, + deleteUser, + getUser + }; +} + +// Individual user operations hook (for use when you don't need the full list) +export function useUser() { + const { request, isLoading, error } = useApiRequest(); + + const getUser = async (userId: number): Promise => { + return await request({ + url: `/api/users/${userId}`, + method: 'get' + }); + }; + + const updateUser = async (userId: number, userData: UserUpdateData): Promise => { + return await request({ + url: `/api/users/${userId}`, + method: 'put', + data: userData + }); + }; + + const deleteUser = async (userId: number): Promise => { + await request({ + url: `/api/users/${userId}`, + method: 'delete' + }); + }; + + return { + getUser, + updateUser, + deleteUser, + isLoading, + error + }; +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 19602fe..2981bd1 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,7 +1,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import App from './App.tsx' -import { AuthProvider } from './auth/auth-provider.tsx'; +import { AuthProvider } from './auth/authProvider.tsx'; createRoot(document.getElementById('root')!).render( diff --git a/src/pages/Dateien/Dateien.module.css b/src/pages/Dateien/Dateien.module.css new file mode 100644 index 0000000..b08da96 --- /dev/null +++ b/src/pages/Dateien/Dateien.module.css @@ -0,0 +1,126 @@ +.dateienContainer { + margin: 51px 49px 0 36px; + display: flex; + padding: 0px 30px 30px 30px; + flex-direction: column; + align-self: stretch; + border-radius: 30px; + border: 1px solid var(--f-1-f-1-f-1, #F1F1F1); + background: var(--Grayscale-True-White, #FFF); + position: relative; + box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10); + max-height: calc(100vh - 100px); + overflow: hidden; +} + +.horizontalLineLight { + width: 100%; + background-color: #F1F1F1; + height: 1px; + margin-top: 90px; + margin-left: -30px; + position: absolute; +} + +.header { + display: flex; + gap: 30px; + align-items: flex-start; + height: 62px; + color: var(--Grayscale-Black, #24262B); + padding-top: 30px; +} + +.datei_hinzufügen_button { + border-radius: 30px; + background: var(--Grayscale-Gray, #E9E9E9); + border: none; + outline: none; + text-align: left; + padding-left: 20px; + padding-right: 20px; + padding-top: 10px; + padding-bottom: 10px; + display: flex; + gap: 10px; + align-items: center; +} + +.datei_hinzufügen_button:hover { + cursor: pointer; +} + +.add_icon { + font-size: 16px; +} + +/* Files table container */ +.filesTable { + width: 100%; + margin-top: 20px; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +/* Table header with exact grid positioning to match the screenshot */ +.tableHeader { + display: grid; + grid-template-columns: 45% 15% 15% 25%; + align-items: center; + height: 40px; + padding: 0px 16px; + position: sticky; + top: 0; + z-index: 10; +} + +/* Header cells with exact positioning */ +.headerCell { + display: flex; + align-items: center; + font-weight: 500; + font-size: 14px; + color: #333; + cursor: pointer; + white-space: nowrap; + padding-left: 0; +} + +/* Adjust first column for icon space */ +.headerCell:nth-child(1) { + padding-left: 30px; +} + +/* Simple sort icon styling */ +.sortIcon { + margin-left: 6px; + font-size: 14px; +} + +/* Modify the file list to use the same grid layout */ +.filesList { + list-style: none; + padding: 0; + margin: 0; + width: 100%; +} + +/* Override the flex layout in DateienItem to force matching the header */ +.filesList li { + display: grid !important; + grid-template-columns: 45% 15% 15% 25%; + border-bottom: 1px solid #f1f1f1; + height: 60px; + padding: 0 16px; + align-items: center; +} + +.error { + color: #d32f2f; + margin: 1rem 0; + padding: 0.5rem; + background-color: #ffebee; + border-radius: 4px; + text-align: center; +} \ No newline at end of file diff --git a/src/pages/Dateien/Dateien.tsx b/src/pages/Dateien/Dateien.tsx new file mode 100644 index 0000000..5005f68 --- /dev/null +++ b/src/pages/Dateien/Dateien.tsx @@ -0,0 +1,166 @@ +import styles from './Dateien.module.css' +import { IoAddCircleOutline } from "react-icons/io5"; +import { FaSort, FaSortUp, FaSortDown } from "react-icons/fa"; +import DateienItem from '../../components/Dateien/DateienItem'; +import DateienUpload from './DateienHinzufügen/DateienUpload'; +import { useState } from 'react'; +import { useUserFiles, useFileOperations } from '../../hooks/useFiles'; + +// Define the file type interface +interface UserFile { + id: number; + file_name: string; + action: string; + created_at: string; + size?: number; +} + +// Sort types +type SortField = 'file_name' | 'action' | 'size' | 'created_at'; +type SortDirection = 'asc' | 'desc'; + +function Dateien() { + const { files, loading, error, refetch } = useUserFiles(); + const [isUploadOpen, setIsUploadOpen] = useState(false); + const { uploadError, downloadError, deleteError } = useFileOperations(); + const [sortField, setSortField] = useState('created_at'); + const [sortDirection, setSortDirection] = useState('desc'); + + // Single function to handle file refresh + const refreshFiles = () => { + console.log('Refreshing files list'); + refetch(); + }; + + const handleFileUpload = async (file: File) => { + console.log('File upload completed:', file.name); + }; + + const handleUploadClose = () => { + setIsUploadOpen(false); + // Refresh files when upload modal is closed + setTimeout(() => { + refreshFiles(); + }, 300); + }; + + const handleFileDeleted = () => { + refreshFiles(); + }; + + // Handle sorting + const handleSort = (field: SortField) => { + if (field === sortField) { + // Toggle direction if same field is clicked + setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); + } else { + // Set new field and default to ascending for most fields, descending for dates + setSortField(field); + setSortDirection(field === 'created_at' ? 'desc' : 'asc'); + } + }; + + // Sort files + const sortedFiles = [...files].sort((a, b) => { + let result = 0; + + switch (sortField) { + case 'file_name': + result = a.file_name.localeCompare(b.file_name); + break; + case 'action': + result = a.action.localeCompare(b.action); + break; + case 'size': + // Handle undefined sizes gracefully + const sizeA = a.size ?? 0; + const sizeB = b.size ?? 0; + result = sizeA - sizeB; + break; + case 'created_at': + result = new Date(a.created_at).getTime() - new Date(b.created_at).getTime(); + break; + } + + // Apply sort direction + return sortDirection === 'asc' ? result : -result; + }); + + // Helper to render sort icon + const renderSortIcon = (field: SortField) => { + if (sortField !== field) return ; + return sortDirection === 'asc' ? + : + ; + }; + + return ( +
    +
    + +
    +
    + + + + {(uploadError || downloadError || deleteError) && ( +

    + {uploadError || downloadError || deleteError} +

    + )} + {loading &&

    Loading files...

    } + {error &&

    Error: {error}

    } + + {!loading && !error && files.length === 0 ? ( +

    No files found.

    + ) : ( +
    + {/* Table Headers */} +
    +
    handleSort('file_name')}> + Name + {renderSortIcon('file_name')} +
    +
    handleSort('action')}> + Typ + {renderSortIcon('action')} +
    +
    handleSort('size')}> + Größe + {renderSortIcon('size')} +
    +
    handleSort('created_at')}> + Datum + {renderSortIcon('created_at')} +
    + +
    + + {/* Files List */} +
      + {sortedFiles.map((file: UserFile) => ( + + ))} +
    +
    + )} +
    + ); +} + +export default Dateien; + diff --git a/src/pages/Dateien/DateienHinzufügen/DateienUpload.module.css b/src/pages/Dateien/DateienHinzufügen/DateienUpload.module.css new file mode 100644 index 0000000..b160943 --- /dev/null +++ b/src/pages/Dateien/DateienHinzufügen/DateienUpload.module.css @@ -0,0 +1,142 @@ +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal { + background: white; + padding: 2rem; + border-radius: 8px; + width: 90%; + max-width: 500px; + position: relative; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.closeButton { + position: absolute; + top: 1rem; + right: 1rem; + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: #666; +} + +.closeButton:hover { + color: #333; +} + +.closeButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.uploadStatus { + padding: 1rem; + border-radius: 4px; + margin-bottom: 1rem; + text-align: center; + font-weight: 500; +} + +.uploadStatus.success { + background-color: rgba(76, 175, 80, 0.1); + color: #388e3c; + border: 1px solid #4caf50; +} + +.uploadStatus.error { + background-color: rgba(244, 67, 54, 0.1); + color: #d32f2f; + border: 1px solid #f44336; +} + +.dropzone { + border: 2px dashed #ccc; + border-radius: 4px; + padding: 2rem; + text-align: center; + cursor: pointer; + margin: 1rem 0; + transition: all 0.3s ease; +} + +.dropzone.active { + border-color: #2196f3; + background-color: rgba(33, 150, 243, 0.1); +} + +.dropzone.uploading { + border-color: #4caf50; + background-color: rgba(76, 175, 80, 0.1); + cursor: wait; +} + +.uploadIcon { + font-size: 3rem; + color: #666; + margin-bottom: 1rem; +} + +.dropzoneText { + color: #666; +} + +.dropzoneText p { + margin: 0.5rem 0; +} + +.browseButton { + background-color: #2196f3; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + margin-top: 0.5rem; +} + +.browseButton:hover { + background-color: #1976d2; +} + +.browseButton:disabled { + background-color: #90caf9; + cursor: not-allowed; +} + +.selectedFile { + margin-top: 1rem; + padding: 1rem; + background-color: #f5f5f5; + border-radius: 4px; +} + +.uploadButton { + background-color: #4caf50; + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 4px; + cursor: pointer; + margin-top: 0.5rem; +} + +.uploadButton:hover { + background-color: #388e3c; +} + +.uploadButton:disabled { + background-color: #a5d6a7; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/pages/Dateien/DateienHinzufügen/DateienUpload.tsx b/src/pages/Dateien/DateienHinzufügen/DateienUpload.tsx new file mode 100644 index 0000000..32a6adc --- /dev/null +++ b/src/pages/Dateien/DateienHinzufügen/DateienUpload.tsx @@ -0,0 +1,123 @@ +import React, { useCallback, useState } from 'react'; +import { useDropzone } from 'react-dropzone'; +import styles from './DateienUpload.module.css'; +import { IoCloudUploadOutline } from "react-icons/io5"; +import { IoClose } from "react-icons/io5"; +import { useFileOperations } from '../../../hooks/useFiles'; + +interface DateienUploadProps { + isOpen: boolean; + onClose: () => void; + onFileUpload: (file: File) => void; +} + +function DateienUpload({ isOpen, onClose, onFileUpload }: DateienUploadProps) { + const [selectedFile, setSelectedFile] = useState(null); + const [isUploading, setIsUploading] = useState(false); + const [uploadStatus, setUploadStatus] = useState<{ success: boolean; message: string } | null>(null); + const { handleFileUpload, uploadError } = useFileOperations(); + + const onDrop = useCallback((acceptedFiles: File[]) => { + if (acceptedFiles.length > 0) { + setSelectedFile(acceptedFiles[0]); + // Clear previous upload status when selecting a new file + setUploadStatus(null); + } + }, []); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + multiple: false + }); + + const handleUpload = async () => { + if (selectedFile) { + setIsUploading(true); + setUploadStatus(null); + + try { + const result = await handleFileUpload(selectedFile); + + if (result.success) { + setUploadStatus({ + success: true, + message: 'Datei erfolgreich hochgeladen!' + }); + onFileUpload(selectedFile); + setSelectedFile(null); + // Close modal after brief success message + setTimeout(() => { + onClose(); + }, 1500); + } else { + setUploadStatus({ + success: false, + message: uploadError || 'Beim Hochladen ist ein Fehler aufgetreten.' + }); + } + } catch (error) { + setUploadStatus({ + success: false, + message: 'Beim Hochladen ist ein unerwarteter Fehler aufgetreten.' + }); + } finally { + setIsUploading(false); + } + } + }; + + if (!isOpen) return null; + + return ( +
    +
    + +

    Datei hochladen

    + + {uploadStatus && ( +
    + {uploadStatus.message} +
    + )} + +
    + + + {isDragActive ? ( +

    Datei hier ablegen...

    + ) : isUploading ? ( +

    Lädt hoch...

    + ) : ( +
    +

    Dateien hierher ziehen

    +

    oder

    + +
    + )} +
    + + {selectedFile && !isUploading && !uploadStatus?.success && ( +
    +

    Ausgewählte Datei: {selectedFile.name}

    + +
    + )} +
    +
    + ); +} + +export default DateienUpload; \ No newline at end of file diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index fb7694b..a6c1ed1 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,12 +1,11 @@ import { useMsal } from '@azure/msal-react'; -import { loginRequest } from '../auth/auth-config'; +import { loginRequest } from '../auth/authConfig'; import { useNavigate, useLocation } from 'react-router-dom'; import { useState, useEffect } from 'react'; import styles from './Login.module.css'; import agentDiagram from '../assets/Frame 43.png'; import logo from '../assets/LogoPowerOn.png'; -import { useAuth } from '../hooks/useAuth'; -import { useMsalAuth } from '../hooks/useMsalAuth'; +import { useAuth, useMsalAuth } from '../hooks/useAuthentication'; function Login() { const { instance, accounts, inProgress } = useMsal(); diff --git a/src/pages/Mitglieder/Mitglieder.module.css b/src/pages/Mitglieder/Mitglieder.module.css new file mode 100644 index 0000000..b22b137 --- /dev/null +++ b/src/pages/Mitglieder/Mitglieder.module.css @@ -0,0 +1,85 @@ +.mitgliederContainer { + margin: 51px 49px 0 36px; + display: flex; + padding: 0px 30px 30px 30px; + flex-direction: column; + align-self: stretch; + border-radius: 30px; + border: 1px solid var(--f-1-f-1-f-1, #F1F1F1); + background: var(--Grayscale-True-White, #FFF); + position: relative; + box-shadow: 0px 2px 6px 0px rgba(194, 194, 194, 0.10); + max-height: calc(100vh - 100px); + overflow: hidden; +} + +.horizontalLineLight { + width: 100%; + background-color: #F1F1F1; + height: 1px; + margin-top: 90px; + margin-left: -30px; + position: absolute; +} + +.header{ + display: flex; + gap: 30px; + align-items: flex-start; + height: 62px; + color: var(--Grayscale-Black, #24262B); + padding-top: 30px; + padding-bottom: 30px; +} + +.mitglieder_hinzufügen_button { + + border-radius: 30px; + background: var(--Grayscale-Gray, #E9E9E9); + + border: none; + outline: none; + text-align: left; + padding-left: 20px; + padding-right: 20px; + padding-top: 10px; + padding-bottom: 10px; + + display: flex; + gap: 10px; + align-items: center; +} + +.mitglieder_hinzufügen_button:hover { + cursor: pointer; +} + +.add_icon { + font-size: 16px; +} + +.membersList { + list-style: none; + padding: 0; + margin: 0; + width: 100%; + overflow-y: auto; /* Enable vertical scrolling */ + /* Space for the header line */ +} + +.membersList li { + display: flex; + align-items: center; + height: 60px; /* Specific height for each item */ + padding: 0 16px; + border-bottom: 1px solid #F1F1F1; + font-size: 16px; + transition: background-color 0.2s ease; + } + +.actions { + display: flex; + gap: 10px; + align-items: center; + justify-content: center; + } \ No newline at end of file diff --git a/src/pages/Mitglieder/Mitglieder.tsx b/src/pages/Mitglieder/Mitglieder.tsx new file mode 100644 index 0000000..839e5e5 --- /dev/null +++ b/src/pages/Mitglieder/Mitglieder.tsx @@ -0,0 +1,43 @@ +import styles from './Mitglieder.module.css' + +import MitgliederItem from '../../components/Mitglieder/MitgliederItem'; +import { IoPersonAddSharp } from "react-icons/io5"; +import { useOrgUsers } from '../../hooks/useUsers'; + +function Mitglieder () { + const { users, loading, error, refetch } = useOrgUsers(); + + return ( +
    +
    + +
    +
    + + {loading ? ( +

    Loading...

    + ) : error ? ( +

    Error: {error}

    + ) : users.length === 0 ? ( +

    No users found.

    + ) : ( +
      + {users.map((user) => ( + + ))} +
    + )} +
    + ); +} + +export default Mitglieder; + diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index bebfa6d..9de5715 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -3,9 +3,7 @@ import { useNavigate } from 'react-router-dom'; import styles from './Register.module.css'; import logo from '../assets/LogoPowerOn.png'; import agentDiagram from '../assets/Frame 43.png'; -import { useRegister } from '../hooks/useRegister'; -import { useMsalRegister } from '../hooks/useMsalRegister'; - +import { useRegister, useMsalRegister } from '../hooks/useAuthentication'; interface RegisterFormData { username: string;