diff --git a/index.html b/index.html index 54bed19..8ca96ed 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,13 @@ - + <%- VITE_APP_NAME %> + + + +
diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..9d36f52 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/logos/Frame 43.png b/public/logos/Frame 43.png deleted file mode 100644 index 309d134..0000000 Binary files a/public/logos/Frame 43.png and /dev/null differ diff --git a/public/logos/PowerOn Details.PNG b/public/logos/PowerOn Details.PNG deleted file mode 100644 index d35eb4f..0000000 Binary files a/public/logos/PowerOn Details.PNG and /dev/null differ diff --git a/public/logos/PowerOn.png b/public/logos/PowerOn.png deleted file mode 100644 index ec1658a..0000000 Binary files a/public/logos/PowerOn.png and /dev/null differ diff --git a/public/logos/PowerOn_transparent.png b/public/logos/PowerOn_transparent.png deleted file mode 100644 index e8d4904..0000000 Binary files a/public/logos/PowerOn_transparent.png and /dev/null differ diff --git a/public/logos/poweron-logo.png b/public/logos/poweron-logo.png new file mode 100644 index 0000000..2a7aea3 Binary files /dev/null and b/public/logos/poweron-logo.png differ diff --git a/public/logos/spitch-logo.svg b/public/logos/spitch-logo.svg deleted file mode 100644 index ea0f59c..0000000 --- a/public/logos/spitch-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/poweron-home.html b/public/poweron-home.html new file mode 100644 index 0000000..0a3c92a --- /dev/null +++ b/public/poweron-home.html @@ -0,0 +1,192 @@ + + + + + + + PowerOn AI Platform - Home + + + + +
+
+

PowerOn AI Platform

+

Intelligent Workflow Automation & Multi-Agent Collaboration

+
+ +
+

What is PowerOn?

+

+ PowerOn is an advanced AI-powered platform that revolutionizes how businesses manage workflows, + collaborate with AI agents, and automate complex processes. Our platform combines cutting-edge + artificial intelligence with intuitive workflow design tools to help organizations work smarter, + not harder. +

+
+ +
+

Core Capabilities

+
+
+

AI Agent Management

+

Create, configure, and manage multiple AI agents for different business tasks and workflows.

+
+
+

Workflow Automation

+

Design and execute complex business processes with drag-and-drop workflow builder.

+
+
+

Document Processing

+

Intelligent document extraction, analysis, and generation powered by AI.

+
+
+

Multi-Platform Integration

+

Seamlessly connect with Microsoft 365, SharePoint, Outlook, and web services.

+
+
+
+ +
+

Who Benefits from PowerOn?

+

+ PowerOn is designed for businesses of all sizes that want to leverage AI to streamline operations, + improve productivity, and reduce manual workload. Whether you're managing customer relationships, + processing documents, or coordinating team workflows, PowerOn provides the tools you need to succeed + in the AI-powered future. +

+
+ +
+

Key Benefits

+ +
+ + + + +
+ + diff --git a/public/poweron-privacy.html b/public/poweron-privacy.html new file mode 100644 index 0000000..1045b30 --- /dev/null +++ b/public/poweron-privacy.html @@ -0,0 +1,290 @@ + + + + + + PowerOn AI Platform - Privacy Policy + + + + +
+
+

Privacy Policy

+

PowerOn AI Platform - Data Protection & Privacy

+
+ +
+ Last Updated: August 2025 +
+ +
+

Introduction

+

+ PowerOn AI Platform ("we," "our," or "us") is committed to protecting your privacy and ensuring + the security of your personal information. This Privacy Policy explains how we collect, use, + disclose, and safeguard your information when you use our AI-powered workflow automation platform. +

+
+ +
+

Information We Collect

+ +

Personal Information

+

We may collect the following types of personal information:

+ + +

Usage Information

+

We automatically collect information about how you use our platform:

+ + +

Technical Information

+

We collect technical information to ensure platform functionality:

+ +
+ +
+

How We Use Your Information

+

We use the collected information for the following purposes:

+ +
+ +
+

Data Sharing and Disclosure

+

We do not sell, trade, or rent your personal information to third parties. We may share your information only in the following circumstances:

+ +
+

Service Providers

+

We work with trusted third-party service providers who assist us in operating our platform, such as cloud hosting services, payment processors, and AI model providers. These providers are contractually obligated to protect your information.

+
+ +
+

Legal Requirements

+

We may disclose your information if required by law, court order, or government regulation, or to protect our rights, property, or safety.

+
+ +
+

Business Transfers

+

In the event of a merger, acquisition, or sale of assets, your information may be transferred as part of the business transaction.

+
+
+ +
+

Data Security

+

We implement comprehensive security measures to protect your information:

+ +
+ +
+

Your Rights and Choices

+

You have the following rights regarding your personal information:

+ +
+ +
+

Data Retention

+

We retain your personal information only as long as necessary to:

+ +

When we no longer need your information, we securely delete or anonymize it.

+
+ +
+

International Data Transfers

+

Your information may be transferred to and processed in countries other than your own. We ensure that such transfers comply with applicable data protection laws and implement appropriate safeguards to protect your information.

+
+ +
+

Children's Privacy

+

Our platform is not intended for use by children under the age of 13. We do not knowingly collect personal information from children under 13. If you believe we have collected such information, please contact us immediately.

+
+ +
+

Changes to This Policy

+

We may update this Privacy Policy from time to time. We will notify you of any material changes by posting the new policy on our platform and updating the "Last Updated" date. Your continued use of our platform after such changes constitutes acceptance of the updated policy.

+
+ +
+

Contact Us

+

If you have any questions about this Privacy Policy or our data practices, please contact us:

+
+

Email: privacy@poweron-ai.com

+

Address: PowerOn AI Platform, Privacy Team

+
+
+ + + + +
+ + diff --git a/public/poweron-terms.html b/public/poweron-terms.html new file mode 100644 index 0000000..c9e057d --- /dev/null +++ b/public/poweron-terms.html @@ -0,0 +1,333 @@ + + + + + + PowerOn AI Platform - Terms of Service + + + + +
+
+

Terms of Service

+

PowerOn AI Platform - Service Agreement & User Terms

+
+ +
+ Last Updated: August 2025 +
+ +
+

Acceptance of Terms

+

+ By accessing or using the PowerOn AI Platform ("Platform"), you agree to be bound by these Terms of Service + ("Terms"). If you do not agree to these Terms, you must not use our Platform. These Terms constitute a + legally binding agreement between you and PowerOn AI Platform ("we," "our," or "us"). +

+
+ +
+

Description of Service

+

+ PowerOn AI Platform is an AI-powered workflow automation and multi-agent collaboration platform that enables + users to create, manage, and execute automated business processes using artificial intelligence agents. +

+

Our Platform includes the following services:

+ +
+ +
+

User Accounts and Registration

+ +

Account Creation

+

To use our Platform, you must create an account by providing accurate, current, and complete information. You are responsible for maintaining the confidentiality of your account credentials.

+ +

Account Security

+

You are responsible for all activities that occur under your account. You must immediately notify us of any unauthorized use of your account or any other security breach.

+ +

Account Termination

+

We reserve the right to terminate or suspend your account at any time for violation of these Terms or for any other reason at our sole discretion.

+
+ +
+

Acceptable Use Policy

+

You agree to use our Platform only for lawful purposes and in accordance with these Terms. You agree not to:

+ +
+

Prohibited Activities

+
    +
  • Use the Platform for any illegal or unauthorized purpose
  • +
  • Violate any applicable laws or regulations
  • +
  • Infringe upon the intellectual property rights of others
  • +
  • Attempt to gain unauthorized access to our systems
  • +
  • Interfere with or disrupt the Platform's operation
  • +
  • Use the Platform to transmit harmful or malicious code
  • +
  • Harass, abuse, or harm other users
  • +
+
+
+ +
+

User Content and Data

+ +

Content Ownership

+

You retain ownership of any content, data, or information you upload, create, or process through our Platform ("User Content").

+ +

Content License

+

By using our Platform, you grant us a limited, non-exclusive license to use your User Content solely for the purpose of providing our services to you.

+ +

Content Responsibility

+

You are solely responsible for the accuracy, legality, and appropriateness of your User Content. We do not review or monitor User Content and are not responsible for its content.

+
+ +
+

Service Availability and Limitations

+ +
+

Service Availability

+

We strive to maintain high service availability but do not guarantee uninterrupted access to our Platform. We may perform maintenance, updates, or modifications that may temporarily affect service availability.

+
+ +
+

Service Limitations

+

Our Platform is subject to reasonable usage limits and technical constraints. We reserve the right to implement usage limits, rate limiting, or other restrictions to ensure fair usage and system stability.

+
+
+ +
+

Intellectual Property Rights

+

+ The Platform, including its software, design, content, and functionality, is owned by PowerOn AI Platform + and is protected by intellectual property laws. You may not copy, modify, distribute, or create derivative + works based on our Platform without our express written consent. +

+
+ +
+

Third-Party Services and Integrations

+

+ Our Platform may integrate with third-party services, applications, or platforms. We are not responsible + for the availability, accuracy, or content of these third-party services. Your use of third-party services + is subject to their respective terms of service and privacy policies. +

+
+ +
+

Payment Terms

+ +

Pricing and Billing

+

Service pricing is available on our Platform and may be subject to change. We will provide reasonable notice of any price changes.

+ +

Payment Obligations

+

You agree to pay all fees associated with your use of our Platform. Failure to pay may result in service suspension or termination.

+ +

Refunds

+

Refund policies are determined by your subscription plan and are subject to our discretion and applicable laws.

+
+ +
+

Disclaimers and Limitations of Liability

+ +
+

Service Disclaimers

+

Our Platform is provided "as is" and "as available" without warranties of any kind. We disclaim all warranties, express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement.

+
+ +
+

Limitation of Liability

+

In no event shall PowerOn AI Platform be liable for any indirect, incidental, special, consequential, or punitive damages, including but not limited to loss of profits, data, or use, arising out of or relating to your use of our Platform.

+
+
+ +
+

Indemnification

+

+ You agree to indemnify, defend, and hold harmless PowerOn AI Platform and its officers, directors, + employees, and agents from and against any claims, damages, losses, liabilities, costs, and expenses + arising out of or relating to your use of our Platform or violation of these Terms. +

+
+ +
+

Governing Law and Dispute Resolution

+

+ These Terms are governed by and construed in accordance with the laws of the jurisdiction where + PowerOn AI Platform is incorporated. Any disputes arising from these Terms or your use of our Platform + shall be resolved through binding arbitration in accordance with applicable arbitration rules. +

+
+ +
+

Changes to Terms

+

+ We reserve the right to modify these Terms at any time. We will notify you of any material changes + by posting the updated Terms on our Platform. Your continued use of our Platform after such changes + constitutes acceptance of the updated Terms. +

+
+ +
+

Contact Information

+

If you have any questions about these Terms of Service, please contact us:

+
+

Email: legal@poweron-ai.com

+

Address: PowerOn AI Platform, Legal Department

+
+
+ + + + +
+ + diff --git a/src/App.tsx b/src/App.tsx index 51b89f3..ecf97b9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,6 +35,7 @@ import { FeatureLayout } from './layouts/FeatureLayout'; import { DashboardPage } from './pages/Dashboard'; import { SettingsPage } from './pages/Settings'; import { FeatureViewPage } from './pages/FeatureView'; +import { AdminMandatesPage, AdminUsersPage, AdminRolesPage } from './pages/admin'; function App() { // Load saved theme preference and set app name on app mount @@ -115,9 +116,9 @@ function App() { {/* ADMIN ROUTES (nur SysAdmin) */} {/* ============================================== */} - Admin: Mandanten (TODO)} /> - Admin: Benutzer (TODO)} /> - Admin: Globale Rollen (TODO)} /> + } /> + } /> + } /> diff --git a/src/api/authApi.ts b/src/api/authApi.ts index 05075f4..8343584 100644 --- a/src/api/authApi.ts +++ b/src/api/authApi.ts @@ -85,15 +85,18 @@ export interface UsernameAvailabilityResponse { message: string; } -export interface User { +// User-Typ wird aus userApi.ts importiert +// Hier nur für Rückwärtskompatibilität +export interface AuthUser { id: string; username: string; email: string; fullName: string; language: string; enabled: boolean; - privilege: string; - mandateId: string; + roleLabels?: string[]; + authenticationAuthority: string; + isSysAdmin?: boolean; [key: string]: any; } @@ -138,7 +141,7 @@ export async function loginApi(loginData: LoginRequest): Promise * Fetch current user data * Endpoint: GET /api/local/me | /api/msft/me | /api/google/me */ -export async function fetchCurrentUserApi(authAuthority?: string): Promise { +export async function fetchCurrentUserApi(authAuthority?: string): Promise { let endpoint = '/api/local/me'; if (authAuthority === 'msft') { @@ -147,7 +150,7 @@ export async function fetchCurrentUserApi(authAuthority?: string): Promise endpoint = '/api/google/me'; } - const response = await api.get(endpoint); + const response = await api.get(endpoint); return response.data; } diff --git a/src/api/featuresApi.ts b/src/api/featuresApi.ts index 2ea691f..8ee75fc 100644 --- a/src/api/featuresApi.ts +++ b/src/api/featuresApi.ts @@ -144,7 +144,7 @@ const MOCK_RESPONSE: FeaturesMyResponse = { }; // Flag für Mock-Modus (auf false setzen wenn Backend bereit) -const USE_MOCK = true; +const USE_MOCK = false; // ============================================================================= // API FUNCTIONS diff --git a/src/api/promptApi.ts b/src/api/promptApi.ts index 6204f51..3b5a000 100644 --- a/src/api/promptApi.ts +++ b/src/api/promptApi.ts @@ -62,15 +62,16 @@ export interface PaginatedResponse { } export interface CreatePromptData { - mandateId: string; name: string; content: string; + // mandateId wird nicht mehr vom Client gesendet + // Das Backend bestimmt den Kontext über die instanceId } export interface UpdatePromptData { - mandateId: string; name: string; content: string; + // mandateId wird nicht mehr vom Client gesendet } // Type for the request function passed to API functions diff --git a/src/api/trusteeApi.ts b/src/api/trusteeApi.ts index 013eeb6..bd97bf6 100644 --- a/src/api/trusteeApi.ts +++ b/src/api/trusteeApi.ts @@ -1,3 +1,12 @@ +/** + * Trustee API + * + * API-Funktionen für das Trustee-Feature. + * Alle Endpunkte erfordern eine instanceId für den Feature-Instanz-Kontext. + * + * URL-Struktur: /api/trustee/{instanceId}/{entity} + */ + import { ApiRequestOptions } from '../hooks/useApi'; // ============================================================================ @@ -60,7 +69,7 @@ export interface TrusteeDocument { contractId: string; documentName: string; documentMimeType: string; - documentData?: any; // Binary data, typically not included in list responses + documentData?: any; mandateId?: string; _createdAt?: number; _modifiedAt?: number; @@ -150,16 +159,24 @@ function _buildPaginationParams(params?: PaginationParams): Record return requestParams; } +/** + * Erstellt die Basis-URL für Trustee-Endpunkte + */ +function _getTrusteeBaseUrl(instanceId: string): string { + return `/api/trustee/${instanceId}`; +} + // ============================================================================ // ORGANISATION API // ============================================================================ export async function fetchOrganisations( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteeOrganisation[]> { return await request({ - url: '/api/trustee/organisations', + url: `${_getTrusteeBaseUrl(instanceId)}/organisations`, method: 'get', params: _buildPaginationParams(params) }); @@ -167,11 +184,12 @@ export async function fetchOrganisations( export async function fetchOrganisationById( request: ApiRequestFunction, + instanceId: string, orgId: string ): Promise { try { return await request({ - url: `/api/trustee/organisations/${orgId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`, method: 'get' }); } catch (error: any) { @@ -182,10 +200,11 @@ export async function fetchOrganisationById( export async function createOrganisation( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/organisations', + url: `${_getTrusteeBaseUrl(instanceId)}/organisations`, method: 'post', data }); @@ -193,11 +212,12 @@ export async function createOrganisation( export async function updateOrganisation( request: ApiRequestFunction, + instanceId: string, orgId: string, data: Partial ): Promise { return await request({ - url: `/api/trustee/organisations/${orgId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`, method: 'put', data }); @@ -205,10 +225,11 @@ export async function updateOrganisation( export async function deleteOrganisation( request: ApiRequestFunction, + instanceId: string, orgId: string ): Promise { await request({ - url: `/api/trustee/organisations/${orgId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`, method: 'delete' }); } @@ -219,10 +240,11 @@ export async function deleteOrganisation( export async function fetchRoles( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteeRole[]> { return await request({ - url: '/api/trustee/roles', + url: `${_getTrusteeBaseUrl(instanceId)}/roles`, method: 'get', params: _buildPaginationParams(params) }); @@ -230,11 +252,12 @@ export async function fetchRoles( export async function fetchRoleById( request: ApiRequestFunction, + instanceId: string, roleId: string ): Promise { try { return await request({ - url: `/api/trustee/roles/${roleId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`, method: 'get' }); } catch (error: any) { @@ -245,10 +268,11 @@ export async function fetchRoleById( export async function createRole( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/roles', + url: `${_getTrusteeBaseUrl(instanceId)}/roles`, method: 'post', data }); @@ -256,11 +280,12 @@ export async function createRole( export async function updateRole( request: ApiRequestFunction, + instanceId: string, roleId: string, data: Partial ): Promise { return await request({ - url: `/api/trustee/roles/${roleId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`, method: 'put', data }); @@ -268,10 +293,11 @@ export async function updateRole( export async function deleteRole( request: ApiRequestFunction, + instanceId: string, roleId: string ): Promise { await request({ - url: `/api/trustee/roles/${roleId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`, method: 'delete' }); } @@ -282,10 +308,11 @@ export async function deleteRole( export async function fetchAccess( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteeAccess[]> { return await request({ - url: '/api/trustee/access', + url: `${_getTrusteeBaseUrl(instanceId)}/access`, method: 'get', params: _buildPaginationParams(params) }); @@ -293,11 +320,12 @@ export async function fetchAccess( export async function fetchAccessById( request: ApiRequestFunction, + instanceId: string, accessId: string ): Promise { try { return await request({ - url: `/api/trustee/access/${accessId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`, method: 'get' }); } catch (error: any) { @@ -308,30 +336,33 @@ export async function fetchAccessById( export async function fetchAccessByOrganisation( request: ApiRequestFunction, + instanceId: string, orgId: string ): Promise { return await request({ - url: `/api/trustee/access/organisation/${orgId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/access/organisation/${orgId}`, method: 'get' }); } export async function fetchAccessByUser( request: ApiRequestFunction, + instanceId: string, userId: string ): Promise { return await request({ - url: `/api/trustee/access/user/${userId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/access/user/${userId}`, method: 'get' }); } export async function createAccess( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/access', + url: `${_getTrusteeBaseUrl(instanceId)}/access`, method: 'post', data }); @@ -339,11 +370,12 @@ export async function createAccess( export async function updateAccess( request: ApiRequestFunction, + instanceId: string, accessId: string, data: Partial ): Promise { return await request({ - url: `/api/trustee/access/${accessId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`, method: 'put', data }); @@ -351,10 +383,11 @@ export async function updateAccess( export async function deleteAccess( request: ApiRequestFunction, + instanceId: string, accessId: string ): Promise { await request({ - url: `/api/trustee/access/${accessId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`, method: 'delete' }); } @@ -365,10 +398,11 @@ export async function deleteAccess( export async function fetchContracts( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteeContract[]> { return await request({ - url: '/api/trustee/contracts', + url: `${_getTrusteeBaseUrl(instanceId)}/contracts`, method: 'get', params: _buildPaginationParams(params) }); @@ -376,11 +410,12 @@ export async function fetchContracts( export async function fetchContractById( request: ApiRequestFunction, + instanceId: string, contractId: string ): Promise { try { return await request({ - url: `/api/trustee/contracts/${contractId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`, method: 'get' }); } catch (error: any) { @@ -391,20 +426,22 @@ export async function fetchContractById( export async function fetchContractsByOrganisation( request: ApiRequestFunction, + instanceId: string, orgId: string ): Promise { return await request({ - url: `/api/trustee/contracts/organisation/${orgId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/contracts/organisation/${orgId}`, method: 'get' }); } export async function createContract( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/contracts', + url: `${_getTrusteeBaseUrl(instanceId)}/contracts`, method: 'post', data }); @@ -412,11 +449,12 @@ export async function createContract( export async function updateContract( request: ApiRequestFunction, + instanceId: string, contractId: string, data: Partial ): Promise { return await request({ - url: `/api/trustee/contracts/${contractId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`, method: 'put', data }); @@ -424,10 +462,11 @@ export async function updateContract( export async function deleteContract( request: ApiRequestFunction, + instanceId: string, contractId: string ): Promise { await request({ - url: `/api/trustee/contracts/${contractId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`, method: 'delete' }); } @@ -438,10 +477,11 @@ export async function deleteContract( export async function fetchDocuments( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteeDocument[]> { return await request({ - url: '/api/trustee/documents', + url: `${_getTrusteeBaseUrl(instanceId)}/documents`, method: 'get', params: _buildPaginationParams(params) }); @@ -449,11 +489,12 @@ export async function fetchDocuments( export async function fetchDocumentById( request: ApiRequestFunction, + instanceId: string, documentId: string ): Promise { try { return await request({ - url: `/api/trustee/documents/${documentId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`, method: 'get' }); } catch (error: any) { @@ -464,20 +505,22 @@ export async function fetchDocumentById( export async function fetchDocumentsByContract( request: ApiRequestFunction, + instanceId: string, contractId: string ): Promise { return await request({ - url: `/api/trustee/documents/contract/${contractId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/documents/contract/${contractId}`, method: 'get' }); } export async function createDocument( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/documents', + url: `${_getTrusteeBaseUrl(instanceId)}/documents`, method: 'post', data }); @@ -485,11 +528,12 @@ export async function createDocument( export async function updateDocument( request: ApiRequestFunction, + instanceId: string, documentId: string, data: Partial ): Promise { return await request({ - url: `/api/trustee/documents/${documentId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`, method: 'put', data }); @@ -497,10 +541,11 @@ export async function updateDocument( export async function deleteDocument( request: ApiRequestFunction, + instanceId: string, documentId: string ): Promise { await request({ - url: `/api/trustee/documents/${documentId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`, method: 'delete' }); } @@ -511,10 +556,11 @@ export async function deleteDocument( export async function fetchPositions( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteePosition[]> { return await request({ - url: '/api/trustee/positions', + url: `${_getTrusteeBaseUrl(instanceId)}/positions`, method: 'get', params: _buildPaginationParams(params) }); @@ -522,11 +568,12 @@ export async function fetchPositions( export async function fetchPositionById( request: ApiRequestFunction, + instanceId: string, positionId: string ): Promise { try { return await request({ - url: `/api/trustee/positions/${positionId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`, method: 'get' }); } catch (error: any) { @@ -537,30 +584,33 @@ export async function fetchPositionById( export async function fetchPositionsByContract( request: ApiRequestFunction, + instanceId: string, contractId: string ): Promise { return await request({ - url: `/api/trustee/positions/contract/${contractId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/positions/contract/${contractId}`, method: 'get' }); } export async function fetchPositionsByOrganisation( request: ApiRequestFunction, + instanceId: string, orgId: string ): Promise { return await request({ - url: `/api/trustee/positions/organisation/${orgId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/positions/organisation/${orgId}`, method: 'get' }); } export async function createPosition( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/positions', + url: `${_getTrusteeBaseUrl(instanceId)}/positions`, method: 'post', data }); @@ -568,11 +618,12 @@ export async function createPosition( export async function updatePosition( request: ApiRequestFunction, + instanceId: string, positionId: string, data: Partial ): Promise { return await request({ - url: `/api/trustee/positions/${positionId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`, method: 'put', data }); @@ -580,10 +631,11 @@ export async function updatePosition( export async function deletePosition( request: ApiRequestFunction, + instanceId: string, positionId: string ): Promise { await request({ - url: `/api/trustee/positions/${positionId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`, method: 'delete' }); } @@ -594,10 +646,11 @@ export async function deletePosition( export async function fetchPositionDocuments( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteePositionDocument[]> { return await request({ - url: '/api/trustee/position-documents', + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`, method: 'get', params: _buildPaginationParams(params) }); @@ -605,11 +658,12 @@ export async function fetchPositionDocuments( export async function fetchPositionDocumentById( request: ApiRequestFunction, + instanceId: string, linkId: string ): Promise { try { return await request({ - url: `/api/trustee/position-documents/${linkId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`, method: 'get' }); } catch (error: any) { @@ -620,30 +674,33 @@ export async function fetchPositionDocumentById( export async function fetchDocumentsForPosition( request: ApiRequestFunction, + instanceId: string, positionId: string ): Promise { return await request({ - url: `/api/trustee/position-documents/position/${positionId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/position/${positionId}`, method: 'get' }); } export async function fetchPositionsForDocument( request: ApiRequestFunction, + instanceId: string, documentId: string ): Promise { return await request({ - url: `/api/trustee/position-documents/document/${documentId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/document/${documentId}`, method: 'get' }); } export async function createPositionDocument( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/position-documents', + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`, method: 'post', data }); @@ -651,10 +708,11 @@ export async function createPositionDocument( export async function deletePositionDocument( request: ApiRequestFunction, + instanceId: string, linkId: string ): Promise { await request({ - url: `/api/trustee/position-documents/${linkId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`, method: 'delete' }); } diff --git a/src/api/userApi.ts b/src/api/userApi.ts index 6cf36e6..0ba5f24 100644 --- a/src/api/userApi.ts +++ b/src/api/userApi.ts @@ -13,8 +13,10 @@ export interface User { enabled: boolean; roleLabels?: string[]; // Array of role labels from backend (e.g., ["user"]) authenticationAuthority: string; - mandateId: string; - [key: string]: any; // Allow additional properties (may include deprecated 'privilege' from backend) + isSysAdmin?: boolean; // System-Administrator Flag + // mandateId ist nicht mehr Teil des User-Objekts (Multi-Tenant-Konzept) + // Der Mandant-Kontext wird über Feature-Instanzen bestimmt + [key: string]: any; // Allow additional properties } export type UserUpdateData = Partial>; diff --git a/src/components/FormGenerator/ActionButtons/EditActionButton/EditActionButton.tsx b/src/components/FormGenerator/ActionButtons/EditActionButton/EditActionButton.tsx index 3555125..af2e030 100644 --- a/src/components/FormGenerator/ActionButtons/EditActionButton/EditActionButton.tsx +++ b/src/components/FormGenerator/ActionButtons/EditActionButton/EditActionButton.tsx @@ -84,15 +84,23 @@ export function EditActionButton({ const handleClick = async (e: React.MouseEvent) => { e.stopPropagation(); if (!isDisabled && !loading && !isEditing && !internalLoading && !fetchingData && !isPopupOpen) { + // If onEdit callback is provided, call it and return early (custom handling) + // The page will handle opening its own modal/form + if (onEdit) { + setInternalLoading(true); + try { + await onEdit(row); + } finally { + setInternalLoading(false); + } + return; // Don't open the built-in popup when custom onEdit is provided + } + + // Otherwise, use the built-in popup form setInternalLoading(true); setFetchingData(true); try { - // Call the onEdit callback if provided - if (onEdit) { - await onEdit(row); - } - const itemId = (row as any)[idField]; // Fetch current item data - use generic fetch function from hookData diff --git a/src/components/Navigation/MandateNavigation.tsx b/src/components/Navigation/MandateNavigation.tsx index 2986e70..159cd60 100644 --- a/src/components/Navigation/MandateNavigation.tsx +++ b/src/components/Navigation/MandateNavigation.tsx @@ -2,6 +2,7 @@ * MandateNavigation * * Hierarchische Navigation für das Multi-Tenant-System. + * Verwendet TreeNavigation für flexible Baumstruktur. * * Struktur: * - SYSTEM (immer verfügbar) @@ -16,13 +17,13 @@ * - ADMINISTRATION (nur für SysAdmin) */ -import React, { useState } from 'react'; -import { NavLink, useLocation } from 'react-router-dom'; +import React, { useMemo } from 'react'; import { useMandates, useFeatureStore } from '../../stores/featureStore'; +import { useCurrentUser } from '../../hooks/useUsers'; import { FEATURE_REGISTRY, getLabel } from '../../types/mandate'; import type { Mandate, MandateFeature, FeatureInstance } from '../../types/mandate'; -import { FaHome, FaCog, FaChevronDown, FaChevronRight, FaBriefcase, FaRobot, FaPlay } from 'react-icons/fa'; -import { RiAdminFill } from 'react-icons/ri'; +import { FaHome, FaCog, FaBriefcase, FaRobot, FaPlay, FaBuilding, FaUsers, FaUserShield } from 'react-icons/fa'; +import { TreeNavigation, type TreeItem, type TreeNodeItem } from './TreeNavigation'; import styles from './MandateNavigation.module.css'; // ============================================================================= @@ -36,263 +37,93 @@ const FEATURE_ICONS: Record = { }; // ============================================================================= -// SYSTEM SECTION +// HELPER FUNCTIONS // ============================================================================= -const SystemSection: React.FC = () => { - const location = useLocation(); - - return ( -
-
- SYSTEM -
-
- - `${styles.navItem} ${isActive && location.pathname === '/' ? styles.active : ''}` - } - > - - Übersicht - - - `${styles.navItem} ${isActive ? styles.active : ''}` - } - > - - Einstellungen - -
-
- ); -}; - -// ============================================================================= -// INSTANCE NAV GROUP -// ============================================================================= - -interface InstanceNavGroupProps { - instance: FeatureInstance; - mandateId: string; - featureCode: string; -} - -const InstanceNavGroup: React.FC = ({ - instance, - mandateId, - featureCode, -}) => { - const [isExpanded, setIsExpanded] = useState(false); - const location = useLocation(); - - // Prüfe ob wir in dieser Instanz sind +/** + * Convert a FeatureInstance to TreeNodeItem + */ +function instanceToTreeNode( + instance: FeatureInstance, + mandateId: string, + featureCode: string +): TreeNodeItem { const basePath = `/mandates/${mandateId}/${featureCode}/${instance.id}`; - const isInInstance = location.pathname.startsWith(basePath); - // Auto-expand wenn wir in der Instanz sind - React.useEffect(() => { - if (isInInstance && !isExpanded) { - setIsExpanded(true); - } - }, [isInInstance]); - - // Views aus Registry holen + // Get views from registry const featureConfig = FEATURE_REGISTRY[featureCode]; const views = featureConfig?.views || []; - // Nur Views anzeigen für die der User Berechtigung hat + // Filter views based on permissions const visibleViews = views.filter(view => { const viewCode = `${featureCode}-${view.code}`; return instance.permissions?.views?.[viewCode] !== false; }); - return ( -
- - - {isExpanded && ( -
- {visibleViews.map(view => ( - - `${styles.viewItem} ${isActive ? styles.active : ''}` - } - > - {getLabel(view.label)} - - ))} -
- )} -
- ); -}; - -// ============================================================================= -// FEATURE NAV GROUP -// ============================================================================= - -interface FeatureNavGroupProps { - feature: MandateFeature; - mandateId: string; + // Convert views to children + const children: TreeNodeItem[] = visibleViews.map(view => ({ + id: `${instance.id}-${view.code}`, + label: getLabel(view.label), + path: `${basePath}/${view.path}`, + })); + + return { + id: instance.id, + label: instance.instanceLabel, + badge: instance.userRole, + children, + defaultExpanded: false, + }; } -const FeatureNavGroup: React.FC = ({ feature, mandateId }) => { - const [isExpanded, setIsExpanded] = useState(false); - const location = useLocation(); - - // Prüfe ob wir in diesem Feature sind - const featurePath = `/mandates/${mandateId}/${feature.code}`; - const isInFeature = location.pathname.startsWith(featurePath); - - // Auto-expand wenn wir im Feature sind - React.useEffect(() => { - if (isInFeature && !isExpanded) { - setIsExpanded(true); - } - }, [isInFeature]); - +/** + * Convert a MandateFeature to TreeNodeItem + */ +function featureToTreeNode( + feature: MandateFeature, + mandateId: string +): TreeNodeItem | null { if (feature.instances.length === 0) { return null; } - return ( -
- - - {isExpanded && ( -
- {feature.instances.map(instance => ( - - ))} -
- )} -
+ const children = feature.instances.map(instance => + instanceToTreeNode(instance, mandateId, feature.code) ); -}; - -// ============================================================================= -// MANDATE NAV GROUP -// ============================================================================= - -interface MandateNavGroupProps { - mandate: Mandate; + + return { + id: `${mandateId}-${feature.code}`, + label: getLabel(feature.label), + icon: FEATURE_ICONS[feature.code] || , + badge: feature.instances.length, + children, + defaultExpanded: false, + }; } -const MandateNavGroup: React.FC = ({ mandate }) => { - const [isExpanded, setIsExpanded] = useState(true); - const location = useLocation(); - - // Prüfe ob wir in diesem Mandanten sind - const mandatePath = `/mandates/${mandate.id}`; - const isInMandate = location.pathname.startsWith(mandatePath); - +/** + * Convert a Mandate to TreeNodeItem + */ +function mandateToTreeNode(mandate: Mandate): TreeNodeItem | null { if (mandate.features.length === 0) { return null; } - return ( -
- - - {isExpanded && ( -
- {mandate.features.map(feature => ( - - ))} -
- )} -
- ); -}; - -// ============================================================================= -// ADMIN SECTION -// ============================================================================= - -interface AdminSectionProps { - isSysAdmin: boolean; -} - -const AdminSection: React.FC = ({ isSysAdmin }) => { - if (!isSysAdmin) { + const children = mandate.features + .map(feature => featureToTreeNode(feature, mandate.id)) + .filter((node): node is TreeNodeItem => node !== null); + + if (children.length === 0) { return null; } - return ( -
-
- ADMINISTRATION -
-
- - `${styles.navItem} ${isActive ? styles.active : ''}` - } - > - - Mandanten - - - `${styles.navItem} ${isActive ? styles.active : ''}` - } - > - - Benutzer - - - `${styles.navItem} ${isActive ? styles.active : ''}` - } - > - - Globale Rollen - -
-
- ); -}; + return { + id: mandate.id, + label: mandate.name, + children, + defaultExpanded: true, + }; +} // ============================================================================= // EMPTY STATE @@ -314,32 +145,97 @@ const EmptyState: React.FC = () => ( export const MandateNavigation: React.FC = () => { const mandates = useMandates(); const { hasAnyInstance } = useFeatureStore(); + const { user } = useCurrentUser(); - // TODO: Aus Auth-Store holen - const isSysAdmin = false; + // Get isSysAdmin from user data + const isSysAdmin = user?.isSysAdmin ?? false; + + // Build navigation items using TreeNavigation structure + const navigationItems: TreeItem[] = useMemo(() => { + const items: TreeItem[] = []; + + // System section (always visible) + items.push({ + type: 'section', + title: 'SYSTEM', + children: [ + { + id: 'home', + label: 'Übersicht', + icon: , + path: '/', + }, + { + id: 'settings', + label: 'Einstellungen', + icon: , + path: '/settings', + }, + ], + }); + + // Separator + items.push({ type: 'separator' }); + + // Mandate nodes (if user has instances) + if (hasAnyInstance()) { + const mandateNodes = mandates + .map(mandate => mandateToTreeNode(mandate)) + .filter((node): node is TreeNodeItem => node !== null); + + if (mandateNodes.length > 0) { + items.push(...mandateNodes); + } + } + + // Admin section (only for SysAdmin) + if (isSysAdmin) { + items.push({ type: 'separator' }); + items.push({ + type: 'section', + title: 'ADMINISTRATION', + children: [ + { + id: 'admin-mandates', + label: 'Mandanten', + icon: , + path: '/admin/mandates', + }, + { + id: 'admin-users', + label: 'Benutzer', + icon: , + path: '/admin/users', + }, + { + id: 'admin-roles', + label: 'Globale Rollen', + icon: , + path: '/admin/roles', + }, + ], + }); + } + + return items; + }, [mandates, hasAnyInstance, isSysAdmin]); return (
- {/* System-Bereich (immer sichtbar) */} - - - {/* Separator */} -
- - {/* Mandanten & Features */} - {hasAnyInstance() ? ( - mandates.map(mandate => ( - - )) + {hasAnyInstance() || isSysAdmin ? ( + ) : ( - + <> + + + )} - - {/* Separator vor Admin */} - {isSysAdmin &&
} - - {/* Admin-Bereich (nur für SysAdmin) */} -
); }; diff --git a/src/components/Navigation/TreeNavigation/TreeNavigation.module.css b/src/components/Navigation/TreeNavigation/TreeNavigation.module.css new file mode 100644 index 0000000..477fc06 --- /dev/null +++ b/src/components/Navigation/TreeNavigation/TreeNavigation.module.css @@ -0,0 +1,303 @@ +/** + * TreeNavigation Styles + * + * Flexible hierarchical navigation with support for: + * - Dynamic sublevels + * - Sections and separators + * - Various visual states (active, disabled, hover) + */ + +.treeNavigation { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0 0.5rem; +} + +/* ============================================ */ +/* SEPARATOR */ +/* ============================================ */ + +.separator { + height: 1px; + background: var(--border-color, #e0e0e0); + margin: 0.75rem 0.5rem; +} + +/* ============================================ */ +/* SECTION */ +/* ============================================ */ + +.treeSection { + margin-bottom: 0.5rem; +} + +.sectionHeader { + padding: 0.5rem 0.75rem; +} + +.sectionTitle { + font-size: 0.65rem; + font-weight: 600; + letter-spacing: 0.1em; + color: var(--text-tertiary, #888); + text-transform: uppercase; +} + +.sectionContent { + display: flex; + flex-direction: column; + gap: 2px; +} + +/* ============================================ */ +/* TREE NODE */ +/* ============================================ */ + +.treeNodeContainer { + display: flex; + flex-direction: column; +} + +.treeNode { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.5rem 0.75rem; + border: none; + border-radius: 6px; + background: transparent; + cursor: pointer; + text-decoration: none; + font-family: inherit; + text-align: left; + color: var(--text-secondary, #666); + font-size: 0.875rem; + transition: all 0.15s ease; +} + +.treeNode:hover { + background: var(--hover-bg, rgba(0, 0, 0, 0.04)); + color: var(--text-primary, #1a1a1a); +} + +.treeNode.active { + background: var(--primary-light, #e0e7ff); + color: var(--primary-color, #2563eb); + font-weight: 500; +} + +.treeNode.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +/* ============================================ */ +/* LEVEL-SPECIFIC STYLES */ +/* ============================================ */ + +/* Root level (level 0) */ +.levelRoot { + font-size: 0.875rem; + font-weight: 600; + color: var(--text-primary, #1a1a1a); + padding: 0.625rem 0.75rem; +} + +.levelRoot .nodeLabel { + flex: 1; +} + +/* Level 1 */ +.levelOne { + font-size: 0.8125rem; + font-weight: 500; + color: var(--text-secondary, #666); + padding: 0.5rem 0.75rem; +} + +/* Level 2 */ +.levelTwo { + font-size: 0.75rem; + font-weight: 500; + color: var(--text-secondary, #666); + padding: 0.375rem 0.5rem; +} + +/* Level 3 */ +.levelThree { + font-size: 0.75rem; + color: var(--text-secondary, #666); + padding: 0.375rem 0.5rem; +} + +/* Deep levels (4+) */ +.levelDeep { + font-size: 0.6875rem; + color: var(--text-tertiary, #888); + padding: 0.25rem 0.5rem; +} + +/* ============================================ */ +/* NODE CHILDREN (INDENTATION) */ +/* ============================================ */ + +.treeNodeChildren { + margin-left: 0.25rem; + padding-left: 0.75rem; + border-left: 2px solid var(--border-color, #e0e0e0); +} + +/* Active parent highlights the border */ +.treeNodeContainer:has(> .treeNode.active) > .treeNodeChildren { + border-left-color: var(--primary-color, #2563eb); +} + +/* Also highlight if any descendant is active */ +.treeNodeContainer:has(.treeNode.active) > .treeNodeChildren { + border-left-color: var(--primary-light, #93c5fd); +} + +/* ============================================ */ +/* NODE ELEMENTS */ +/* ============================================ */ + +.chevron { + display: flex; + align-items: center; + justify-content: center; + width: 1rem; + height: 1rem; + font-size: 0.625rem; + color: var(--text-tertiary, #888); + flex-shrink: 0; + cursor: pointer; + border-radius: 3px; + transition: background 0.1s ease; +} + +.chevron:hover { + background: var(--hover-bg, rgba(0, 0, 0, 0.06)); +} + +.chevronSpacer { + width: 1rem; + flex-shrink: 0; +} + +.nodeIcon { + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + flex-shrink: 0; + color: inherit; +} + +.nodeLabel { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.nodeBadge { + font-size: 0.625rem; + padding: 0.0625rem 0.375rem; + background: var(--surface-color, #f0f0f0); + border-radius: 9999px; + color: var(--text-tertiary, #888); + text-transform: uppercase; + letter-spacing: 0.025em; + flex-shrink: 0; +} + +/* Badge variants */ +.badgePrimary { + background: var(--primary-color, #2563eb); + color: white; +} + +.badgeSuccess { + background: var(--success-color, #22c55e); + color: white; +} + +.badgeWarning { + background: var(--warning-color, #f59e0b); + color: white; +} + +/* Active node badge */ +.treeNode.active .nodeBadge { + background: var(--primary-color, #2563eb); + color: white; +} + +/* ============================================ */ +/* DARK THEME */ +/* ============================================ */ + +:global(.dark-theme) .separator { + background: var(--border-dark, #333); +} + +:global(.dark-theme) .sectionTitle { + color: var(--text-tertiary-dark, #666); +} + +:global(.dark-theme) .treeNode { + color: var(--text-secondary-dark, #aaa); +} + +:global(.dark-theme) .treeNode:hover { + background: var(--hover-bg-dark, rgba(255, 255, 255, 0.06)); + color: var(--text-primary-dark, #fff); +} + +:global(.dark-theme) .treeNode.active { + background: var(--primary-dark-bg, #1e3a5f); + color: var(--primary-light, #93c5fd); +} + +:global(.dark-theme) .levelRoot { + color: var(--text-primary-dark, #fff); +} + +:global(.dark-theme) .levelOne, +:global(.dark-theme) .levelTwo, +:global(.dark-theme) .levelThree { + color: var(--text-secondary-dark, #aaa); +} + +:global(.dark-theme) .levelDeep { + color: var(--text-tertiary-dark, #888); +} + +:global(.dark-theme) .treeNodeChildren { + border-left-color: var(--border-dark, #444); +} + +:global(.dark-theme) .treeNodeContainer:has(.treeNode.active) > .treeNodeChildren { + border-left-color: var(--primary-light, #93c5fd); +} + +:global(.dark-theme) .nodeBadge { + background: var(--surface-dark, #2a2a2a); + color: var(--text-tertiary-dark, #888); +} + +:global(.dark-theme) .chevron { + color: var(--text-tertiary-dark, #666); +} + +:global(.dark-theme) .chevron:hover { + background: var(--hover-bg-dark, rgba(255, 255, 255, 0.1)); +} + +:global(.dark-theme) .treeNode.active .nodeBadge { + background: var(--primary-color, #2563eb); + color: white; +} diff --git a/src/components/Navigation/TreeNavigation/TreeNavigation.tsx b/src/components/Navigation/TreeNavigation/TreeNavigation.tsx new file mode 100644 index 0000000..0e1af84 --- /dev/null +++ b/src/components/Navigation/TreeNavigation/TreeNavigation.tsx @@ -0,0 +1,378 @@ +/** + * TreeNavigation + * + * A flexible, recursive tree navigation component that supports: + * - Dynamic sublevels of any depth + * - Expandable/collapsible nodes + * - Auto-expand based on active path + * - Customizable icons and badges + * - Section headers + * - NavLink integration with React Router + */ + +import React, { useState, useEffect, ReactNode } from 'react'; +import { NavLink, useLocation } from 'react-router-dom'; +import { FaChevronDown, FaChevronRight } from 'react-icons/fa'; +import styles from './TreeNavigation.module.css'; + +// ============================================================================= +// TYPES +// ============================================================================= + +export interface TreeNodeItem { + /** Unique identifier for this node */ + id: string; + /** Display label */ + label: string; + /** Icon to display (React component or element) */ + icon?: ReactNode; + /** Badge content (e.g., count, role) */ + badge?: string | number; + /** Optional badge style variant */ + badgeVariant?: 'default' | 'primary' | 'success' | 'warning'; + /** Path for navigation (if this is a link) */ + path?: string; + /** Child nodes */ + children?: TreeNodeItem[]; + /** Whether this node is expanded by default */ + defaultExpanded?: boolean; + /** Whether this node can be expanded/collapsed (default: true if has children) */ + expandable?: boolean; + /** Custom onClick handler (overrides navigation) */ + onClick?: () => void; + /** Whether this node is disabled */ + disabled?: boolean; + /** Additional CSS class */ + className?: string; + /** Indent level (auto-calculated) */ + level?: number; + /** Data attribute for testing/identification */ + dataId?: string; +} + +export interface TreeSectionItem { + /** Section type */ + type: 'section'; + /** Section title */ + title: string; + /** Child nodes in this section */ + children: TreeNodeItem[]; + /** Whether this section is initially visible */ + visible?: boolean; +} + +export interface TreeSeparatorItem { + /** Separator type */ + type: 'separator'; +} + +export type TreeItem = TreeNodeItem | TreeSectionItem | TreeSeparatorItem; + +export interface TreeNavigationProps { + /** Array of tree items to render */ + items: TreeItem[]; + /** Whether to auto-expand nodes when their path is active */ + autoExpandActive?: boolean; + /** Callback when a node is clicked */ + onNodeClick?: (node: TreeNodeItem) => void; + /** Maximum depth to render (0 = unlimited) */ + maxDepth?: number; + /** Additional CSS class for the container */ + className?: string; +} + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +/** + * Check if a node or any of its descendants has the active path + */ +function hasActivePath(node: TreeNodeItem, currentPath: string): boolean { + if (node.path && currentPath.startsWith(node.path)) { + return true; + } + if (node.children) { + return node.children.some(child => hasActivePath(child, currentPath)); + } + return false; +} + +/** + * Type guard to check if item is a TreeNodeItem + */ +function isTreeNode(item: TreeItem): item is TreeNodeItem { + return !('type' in item); +} + +/** + * Type guard to check if item is a TreeSectionItem + */ +function isTreeSection(item: TreeItem): item is TreeSectionItem { + return 'type' in item && item.type === 'section'; +} + +/** + * Type guard to check if item is a TreeSeparatorItem + */ +function isTreeSeparator(item: TreeItem): item is TreeSeparatorItem { + return 'type' in item && item.type === 'separator'; +} + +// ============================================================================= +// TREE NODE COMPONENT +// ============================================================================= + +interface TreeNodeProps { + node: TreeNodeItem; + level: number; + autoExpandActive: boolean; + currentPath: string; + onNodeClick?: (node: TreeNodeItem) => void; + maxDepth: number; +} + +const TreeNode: React.FC = ({ + node, + level, + autoExpandActive, + currentPath, + onNodeClick, + maxDepth, +}) => { + const hasChildren = node.children && node.children.length > 0; + const isExpandable = node.expandable !== false && hasChildren; + const shouldAutoExpand = autoExpandActive && hasActivePath(node, currentPath); + + const [isExpanded, setIsExpanded] = useState( + node.defaultExpanded ?? shouldAutoExpand ?? false + ); + + // Auto-expand when path becomes active + useEffect(() => { + if (autoExpandActive && hasActivePath(node, currentPath) && !isExpanded) { + setIsExpanded(true); + } + }, [currentPath, autoExpandActive, node]); + + // Check if this exact node is active + const isActive = node.path ? currentPath === node.path || currentPath.startsWith(node.path + '/') : false; + + // Handle click + const handleClick = (e: React.MouseEvent) => { + if (node.disabled) { + e.preventDefault(); + return; + } + + if (node.onClick) { + e.preventDefault(); + node.onClick(); + return; + } + + if (isExpandable && !node.path) { + // If only expandable (no path), toggle expand + setIsExpanded(!isExpanded); + } else if (isExpandable && node.path) { + // If both expandable and has path, expand on click but allow navigation + if (!isExpanded) { + setIsExpanded(true); + } + } + + if (onNodeClick) { + onNodeClick(node); + } + }; + + // Handle chevron click separately + const handleChevronClick = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsExpanded(!isExpanded); + }; + + // Get level-specific styles + const getLevelClass = () => { + switch (level) { + case 0: return styles.levelRoot; + case 1: return styles.levelOne; + case 2: return styles.levelTwo; + case 3: return styles.levelThree; + default: return styles.levelDeep; + } + }; + + // Render the node content + const nodeContent = ( + <> + {isExpandable && ( + + {isExpanded ? : } + + )} + {!isExpandable && hasChildren === false && ( + + )} + {node.icon && {node.icon}} + {node.label} + {node.badge !== undefined && ( + + {node.badge} + + )} + + ); + + // Determine if we should render as NavLink or button + const nodeClasses = `${styles.treeNode} ${getLevelClass()} ${isActive ? styles.active : ''} ${node.disabled ? styles.disabled : ''} ${node.className || ''}`; + + const nodeElement = node.path ? ( + + {nodeContent} + + ) : ( + + ); + + // Check max depth + const canRenderChildren = maxDepth === 0 || level < maxDepth; + + return ( +
+ {nodeElement} + {isExpanded && hasChildren && canRenderChildren && ( +
+ {node.children!.map((child, index) => ( + + ))} +
+ )} +
+ ); +}; + +// ============================================================================= +// TREE SECTION COMPONENT +// ============================================================================= + +interface TreeSectionProps { + section: TreeSectionItem; + autoExpandActive: boolean; + currentPath: string; + onNodeClick?: (node: TreeNodeItem) => void; + maxDepth: number; +} + +const TreeSection: React.FC = ({ + section, + autoExpandActive, + currentPath, + onNodeClick, + maxDepth, +}) => { + if (section.visible === false) { + return null; + } + + return ( +
+
+ {section.title} +
+
+ {section.children.map((node, index) => ( + + ))} +
+
+ ); +}; + +// ============================================================================= +// MAIN COMPONENT +// ============================================================================= + +export const TreeNavigation: React.FC = ({ + items, + autoExpandActive = true, + onNodeClick, + maxDepth = 0, + className = '', +}) => { + const location = useLocation(); + const currentPath = location.pathname; + + return ( + + ); +}; + +export default TreeNavigation; diff --git a/src/components/Navigation/TreeNavigation/index.ts b/src/components/Navigation/TreeNavigation/index.ts new file mode 100644 index 0000000..a39ae4d --- /dev/null +++ b/src/components/Navigation/TreeNavigation/index.ts @@ -0,0 +1,8 @@ +/** + * TreeNavigation Component Index + * + * Export all tree navigation related types and components + */ + +export { TreeNavigation, type TreeNavigationProps, type TreeItem, type TreeNodeItem, type TreeSectionItem, type TreeSeparatorItem } from './TreeNavigation'; +export { default } from './TreeNavigation'; diff --git a/src/components/Navigation/UserSection.module.css b/src/components/Navigation/UserSection.module.css new file mode 100644 index 0000000..33da0be --- /dev/null +++ b/src/components/Navigation/UserSection.module.css @@ -0,0 +1,324 @@ +/** + * UserSection Styles + */ + +.userSection { + position: relative; + padding: 0.5rem; + border-top: 1px solid var(--border-color, #e0e0e0); +} + +.userButton { + display: flex; + align-items: center; + gap: 0.75rem; + width: 100%; + padding: 0.5rem; + border: none; + border-radius: 8px; + background: transparent; + cursor: pointer; + transition: background 0.2s; + text-align: left; +} + +.userButton:hover { + background: var(--hover-bg, rgba(0, 0, 0, 0.05)); +} + +.avatar { + flex-shrink: 0; + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--primary-color, #2563eb); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; +} + +.userInfo { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.userName { + font-size: 0.875rem; + font-weight: 500; + color: var(--text-primary, #1a1a1a); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.userEmail { + font-size: 0.75rem; + color: var(--text-secondary, #666); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chevron { + flex-shrink: 0; + font-size: 0.625rem; + color: var(--text-tertiary, #888); +} + +/* Menu */ +.menu { + position: absolute; + bottom: 100%; + left: 0.5rem; + right: 0.5rem; + margin-bottom: 0.25rem; + padding: 0.25rem; + background: var(--bg-primary, #ffffff); + border: 1px solid var(--border-color, #e0e0e0); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + z-index: 100; +} + +.menuItem { + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; + padding: 0.5rem 0.75rem; + border: none; + border-radius: 6px; + background: transparent; + font-size: 0.875rem; + color: var(--text-primary, #1a1a1a); + cursor: pointer; + transition: background 0.2s; + text-align: left; +} + +.menuItem:hover { + background: var(--hover-bg, rgba(0, 0, 0, 0.05)); +} + +.menuItem:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.menuIcon { + font-size: 1rem; +} + +.menuDivider { + height: 1px; + margin: 0.25rem 0; + background: var(--border-color, #e0e0e0); +} + +/* Dark Theme */ +:global(.dark-theme) .userSection { + border-top-color: var(--border-dark, #333); +} + +:global(.dark-theme) .userButton:hover { + background: var(--hover-bg-dark, rgba(255, 255, 255, 0.05)); +} + +:global(.dark-theme) .userName { + color: var(--text-primary-dark, #ffffff); +} + +:global(.dark-theme) .userEmail { + color: var(--text-secondary-dark, #aaa); +} + +:global(.dark-theme) .chevron { + color: var(--text-tertiary-dark, #888); +} + +:global(.dark-theme) .menu { + background: var(--surface-dark, #1a1a1a); + border-color: var(--border-dark, #444); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +:global(.dark-theme) .menuItem { + color: var(--text-primary-dark, #ffffff); +} + +:global(.dark-theme) .menuItem:hover { + background: var(--hover-bg-dark, rgba(255, 255, 255, 0.1)); +} + +:global(.dark-theme) .menuDivider { + background: var(--border-dark, #444); +} + +/* Modal Overlay */ +.modalOverlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; +} + +.modal { + background: var(--bg-primary, #ffffff); + border-radius: 12px; + max-width: 700px; + width: 100%; + max-height: 80vh; + overflow: hidden; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2); +} + +.modalHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color, #e0e0e0); +} + +.modalHeader h2 { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary, #1a1a1a); +} + +.modalClose { + background: transparent; + border: none; + font-size: 1.25rem; + cursor: pointer; + color: var(--text-tertiary, #888); + padding: 0.25rem; + line-height: 1; +} + +.modalClose:hover { + color: var(--text-primary, #1a1a1a); +} + +.modalContent { + padding: 1.5rem; + overflow-y: auto; + max-height: calc(80vh - 60px); +} + +.legalSection { + margin-bottom: 1.5rem; +} + +.legalSection h3 { + color: var(--text-primary, #1a1a1a); + font-size: 1.125rem; + font-weight: 600; + margin: 0 0 1rem 0; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--primary-color, #F25843); +} + +.legalSection h4 { + color: var(--text-primary, #1a1a1a); + font-size: 0.9375rem; + font-weight: 600; + margin: 1rem 0 0.5rem 0; +} + +.legalSection p { + color: var(--text-secondary, #666); + font-size: 0.875rem; + line-height: 1.6; + margin: 0 0 0.75rem 0; +} + +.legalSection ul { + margin: 0 0 0.75rem 1.5rem; + padding: 0; +} + +.legalSection li { + color: var(--text-secondary, #666); + font-size: 0.875rem; + line-height: 1.6; + margin-bottom: 0.5rem; +} + +.legalLinks { + display: flex; + flex-wrap: wrap; + gap: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border-color, #e0e0e0); +} + +.legalLinks a { + color: var(--primary-color, #F25843); + text-decoration: none; + font-size: 0.875rem; + font-weight: 500; + padding: 0.5rem 1rem; + border-radius: 6px; + background: var(--primary-dark-bg, rgba(242, 88, 67, 0.1)); + transition: background 0.2s; +} + +.legalLinks a:hover { + background: var(--primary-light, rgba(242, 88, 67, 0.2)); +} + +/* Dark Theme Modal */ +:global(.dark-theme) .modal { + background: var(--surface-dark, #1a1a1a); +} + +:global(.dark-theme) .modalHeader { + border-bottom-color: var(--border-dark, #333); +} + +:global(.dark-theme) .modalHeader h2 { + color: var(--text-primary-dark, #ffffff); +} + +:global(.dark-theme) .modalClose { + color: var(--text-tertiary-dark, #888); +} + +:global(.dark-theme) .modalClose:hover { + color: var(--text-primary-dark, #ffffff); +} + +:global(.dark-theme) .legalSection h3 { + color: var(--text-primary-dark, #ffffff); +} + +:global(.dark-theme) .legalSection h4 { + color: var(--text-primary-dark, #ffffff); +} + +:global(.dark-theme) .legalSection p, +:global(.dark-theme) .legalSection li { + color: var(--text-secondary-dark, #aaa); +} + +:global(.dark-theme) .legalLinks { + border-top-color: var(--border-dark, #333); +} + +:global(.dark-theme) .legalLinks a { + color: var(--primary-light, #FF9A8A); +} diff --git a/src/components/Navigation/UserSection.tsx b/src/components/Navigation/UserSection.tsx new file mode 100644 index 0000000..3fd0c7c --- /dev/null +++ b/src/components/Navigation/UserSection.tsx @@ -0,0 +1,155 @@ +/** + * UserSection Component + * + * Zeigt Benutzerinformationen und Logout-Button in der Sidebar. + */ + +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useCurrentUser } from '../../hooks/useUsers'; +import { useMsal } from '@azure/msal-react'; +import styles from './UserSection.module.css'; + +export const UserSection: React.FC = () => { + const { user, logout } = useCurrentUser(); + const { instance: msalInstance } = useMsal(); + const navigate = useNavigate(); + const [isLoggingOut, setIsLoggingOut] = useState(false); + const [showMenu, setShowMenu] = useState(false); + const [showLegalModal, setShowLegalModal] = useState(false); + + const handleLogout = async () => { + setIsLoggingOut(true); + try { + await logout(msalInstance); + } catch (error) { + console.error('Logout failed:', error); + setIsLoggingOut(false); + } + }; + + const handleSettings = () => { + navigate('/settings'); + setShowMenu(false); + }; + + const handleLegal = () => { + setShowLegalModal(true); + setShowMenu(false); + }; + + if (!user) { + return null; + } + + // Initialen für Avatar + const initials = user.fullName + ? user.fullName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) + : user.username.slice(0, 2).toUpperCase(); + + return ( +
+ + + {showMenu && ( +
+ + + + +
+ + +
+ )} + + {/* Legal Modal */} + {showLegalModal && ( +
setShowLegalModal(false)}> +
e.stopPropagation()}> +
+

Rechtliche Hinweise

+ +
+
+
+

Datenverarbeitung und KI-Nutzung

+ +

1. Einwilligung zur Datenverarbeitung

+

Mit der Nutzung dieser Anwendung stimmen Sie zu und erklären sich mit den folgenden Bedingungen zur Verarbeitung Ihrer Daten durch künstliche Intelligenz einverstanden:

+
    +
  • Sie autorisieren die Erfassung, Verarbeitung, Übertragung und Speicherung aller Daten, die Sie bei der Nutzung unserer Dienste bereitstellen.
  • +
  • Nutzerdaten können an Drittanbieter von künstlicher Intelligenz übertragen werden (z.B. OpenAI).
  • +
  • Diese Einwilligung erstreckt sich auf alle Inhalte, einschließlich Text, Bilder, Dokumente und Gesprächsverläufe.
  • +
+ +

2. Anerkennung der KI-Verarbeitungsrisiken

+
    +
  • KI-Systeme können unerwartete oder ungenaue Ausgaben erzeugen.
  • +
  • KI-Dienste können Daten gemäß ihren eigenen Nutzungsbedingungen speichern oder daraus lernen.
  • +
  • Trotz Sicherheitsmaßnahmen können Daten anfällig für unbefugten Zugriff sein.
  • +
+ +

3. Haftungsausschluss

+

Im größtmöglichen Umfang verzichten Sie auf Ansprüche, die sich aus der KI-Verarbeitung ergeben, einschließlich Datenverletzungen und unbeabsichtigter Offenlegung.

+
+ + +
+
+
+ )} +
+ ); +}; + +export default UserSection; diff --git a/src/components/Sidebar/SidebarUser.tsx b/src/components/Sidebar/SidebarUser.tsx index 68aa4dd..cc4ae9d 100644 --- a/src/components/Sidebar/SidebarUser.tsx +++ b/src/components/Sidebar/SidebarUser.tsx @@ -70,7 +70,7 @@ const SidebarUser: React.FC = ({ isMinimized = false }) => { enabled: cached.enabled ?? true, // Assume enabled if logged in roleLabels: cached.roleLabels || [], authenticationAuthority: cached.authenticationAuthority || 'local', - mandateId: cached.mandateId || '' + isSysAdmin: cached.isSysAdmin || false }; setUser(userData); setUserError(null); @@ -99,7 +99,7 @@ const SidebarUser: React.FC = ({ isMinimized = false }) => { enabled: cached.enabled ?? true, roleLabels: cached.roleLabels || [], authenticationAuthority: cached.authenticationAuthority || 'local', - mandateId: cached.mandateId || '' + isSysAdmin: cached.isSysAdmin || false }; setUser(userData); setUserError(null); diff --git a/src/hooks/useCurrentInstance.ts b/src/hooks/useCurrentInstance.ts index 1efd0d6..d752521 100644 --- a/src/hooks/useCurrentInstance.ts +++ b/src/hooks/useCurrentInstance.ts @@ -15,12 +15,8 @@ import type { FeatureInstance, Mandate, MandateFeature } from '../types/mandate' // URL PARAMETER TYPES // ============================================================================= -export interface FeatureRouteParams { - mandateId?: string; - featureCode?: string; - instanceId?: string; - '*'?: string; // Wildcard für Sub-Pfade -} +// Route-Parameter werden als Record erwartet +// Wir verwenden daher einen einfachen Typ-Alias // ============================================================================= // RETURN TYPES @@ -63,7 +59,7 @@ export interface CurrentInstanceContext { * ``` */ export function useCurrentInstance(): CurrentInstanceContext { - const params = useParams(); + const params = useParams(); const { getMandateById, getFeatureByCode, getInstanceById, loading } = useFeatureStore(); const mandateId = params.mandateId; @@ -108,7 +104,7 @@ export function useInstance(): FeatureInstance | undefined { * Hook für die Instanz-ID aus der URL */ export function useInstanceId(): string | undefined { - const params = useParams(); + const params = useParams(); return params.instanceId; } @@ -116,7 +112,7 @@ export function useInstanceId(): string | undefined { * Hook für den Feature-Code aus der URL */ export function useFeatureCode(): string | undefined { - const params = useParams(); + const params = useParams(); return params.featureCode; } @@ -124,7 +120,7 @@ export function useFeatureCode(): string | undefined { * Hook für die Mandate-ID aus der URL */ export function useMandateId(): string | undefined { - const params = useParams(); + const params = useParams(); return params.mandateId; } diff --git a/src/hooks/useInstancePermissions.ts b/src/hooks/useInstancePermissions.tsx similarity index 98% rename from src/hooks/useInstancePermissions.ts rename to src/hooks/useInstancePermissions.tsx index f16a219..c048d73 100644 --- a/src/hooks/useInstancePermissions.ts +++ b/src/hooks/useInstancePermissions.tsx @@ -5,7 +5,7 @@ * Die Berechtigungen werden summarisch pro Instanz geladen (kein einzelner API-Call pro Check). */ -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; import { useCurrentInstance } from './useCurrentInstance'; import type { TablePermission, @@ -27,10 +27,6 @@ const NO_ACCESS_TABLE: TablePermission = { delete: 'n', }; -const NO_ACCESS_FIELD: FieldPermission = { - read: false, - write: false, -}; // ============================================================================= // TABLE PERMISSION HOOKS diff --git a/src/hooks/useMandates.ts b/src/hooks/useMandates.ts new file mode 100644 index 0000000..e3848fe --- /dev/null +++ b/src/hooks/useMandates.ts @@ -0,0 +1,235 @@ +/** + * useMandates Hook + * + * Hook für die Verwaltung von Mandanten (Mandates) im Admin-Bereich. + * Folgt dem gleichen Pattern wie useOrgUsers. + */ + +import { useState, useEffect, useCallback } from 'react'; +import { useApiRequest } from './useApi'; +import api from '../api'; +import { usePermissions, type UserPermissions } from './usePermissions'; +import { + fetchMandates as fetchMandatesApi, + fetchMandateById as fetchMandateByIdApi, + createMandate as createMandateApi, + updateMandate as updateMandateApi, + deleteMandate as deleteMandateApi, + type Mandate, + type MandateUpdateData, + type PaginationParams +} from '../api/mandateApi'; + +// Re-export types +export type { Mandate, MandateUpdateData, PaginationParams }; + +export interface AttributeDefinition { + name: string; + type: string; + label: string; + description?: string; + required?: boolean; + default?: any; + options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string; + sortable?: boolean; + filterable?: boolean; + searchable?: boolean; + width?: number; + minWidth?: number; + maxWidth?: number; + readonly?: boolean; + editable?: boolean; +} + +/** + * Hook for managing mandates in admin panel + */ +export function useAdminMandates() { + const [mandates, setMandates] = useState([]); + const [attributes, setAttributes] = useState([]); + const [permissions, setPermissions] = useState(null); + const [pagination, setPagination] = useState<{ + currentPage: number; + pageSize: number; + totalItems: number; + totalPages: number; + } | null>(null); + const { request, isLoading: loading, error } = useApiRequest(); + const { checkPermission } = usePermissions(); + + // Fetch attributes from backend + const fetchAttributes = useCallback(async () => { + try { + const response = await api.get('/api/attributes/Mandate'); + + let attrs: AttributeDefinition[] = []; + if (response.data?.attributes && Array.isArray(response.data.attributes)) { + attrs = response.data.attributes; + } else if (Array.isArray(response.data)) { + attrs = response.data; + } else if (response.data && typeof response.data === 'object') { + const keys = Object.keys(response.data); + for (const key of keys) { + if (Array.isArray(response.data[key])) { + attrs = response.data[key]; + break; + } + } + } + + setAttributes(attrs); + return attrs; + } catch (error: any) { + if (error.response?.status === 429) { + console.warn('Rate limit exceeded while fetching mandate attributes.'); + } else if (error.response?.status !== 401) { + console.error('Error fetching mandate attributes:', error); + } + setAttributes([]); + return []; + } + }, []); + + // Fetch permissions + const fetchPermissions = useCallback(async () => { + try { + const perms = await checkPermission('DATA', 'Mandate'); + setPermissions(perms); + return perms; + } catch (error: any) { + console.error('Error fetching mandate permissions:', error); + const defaultPerms: UserPermissions = { + view: false, + read: 'n', + create: 'n', + update: 'n', + delete: 'n', + }; + setPermissions(defaultPerms); + return defaultPerms; + } + }, [checkPermission]); + + // Fetch mandates + const fetchMandates = useCallback(async (params?: PaginationParams) => { + try { + const data = await fetchMandatesApi(request, params); + + if (data && typeof data === 'object' && 'items' in data) { + const items = Array.isArray(data.items) ? data.items : []; + setMandates(items); + if (data.pagination) { + setPagination(data.pagination); + } + } else { + const items = Array.isArray(data) ? data : []; + setMandates(items); + setPagination(null); + } + } catch (error: any) { + setMandates([]); + setPagination(null); + } + }, [request]); + + // Optimistic updates + const removeOptimistically = (mandateId: string) => { + setMandates(prev => prev.filter(m => m.id !== mandateId)); + }; + + const updateOptimistically = (mandateId: string, updateData: Partial) => { + setMandates(prev => + prev.map(m => m.id === mandateId ? { ...m, ...updateData } : m) + ); + }; + + // Fetch single mandate + const fetchMandateById = useCallback(async (mandateId: string): Promise => { + return await fetchMandateByIdApi(request, mandateId); + }, [request]); + + // Generate columns from attributes + const columns = attributes.map(attr => ({ + key: attr.name, + label: attr.label || attr.name, + type: attr.type as any, + sortable: attr.sortable !== false, + filterable: attr.filterable !== false, + searchable: attr.searchable !== false, + width: attr.width || 150, + minWidth: attr.minWidth || 100, + maxWidth: attr.maxWidth || 400, + })); + + // Create mandate + const handleCreate = useCallback(async (mandateData: Partial): Promise => { + try { + await createMandateApi(request, mandateData); + await fetchMandates(); + return true; + } catch (error: any) { + console.error('Error creating mandate:', error); + return false; + } + }, [request, fetchMandates]); + + // Update mandate + const handleUpdate = useCallback(async (mandateId: string, updateData: MandateUpdateData): Promise => { + try { + updateOptimistically(mandateId, updateData); + await updateMandateApi(request, mandateId, updateData); + return true; + } catch (error: any) { + console.error('Error updating mandate:', error); + await fetchMandates(); + return false; + } + }, [request, fetchMandates]); + + // Delete mandate + const handleDelete = useCallback(async (mandateId: string): Promise => { + try { + removeOptimistically(mandateId); + await deleteMandateApi(request, mandateId); + return true; + } catch (error: any) { + console.error('Error deleting mandate:', error); + await fetchMandates(); + return false; + } + }, [request, fetchMandates]); + + // Inline update + const handleInlineUpdate = useCallback(async ( + mandateId: string, + updateData: Partial + ): Promise => { + await handleUpdate(mandateId, updateData); + }, [handleUpdate]); + + // Load data on mount + useEffect(() => { + fetchAttributes(); + fetchPermissions(); + fetchMandates(); + }, []); + + return { + mandates, + attributes, + columns, + permissions, + pagination, + loading, + error, + refetch: fetchMandates, + fetchMandateById, + handleCreate, + handleUpdate, + handleDelete, + handleInlineUpdate, + updateOptimistically, + }; +} + +export default useAdminMandates; diff --git a/src/hooks/usePrompts.ts b/src/hooks/usePrompts.ts index dbd8bb7..6d0cabb 100644 --- a/src/hooks/usePrompts.ts +++ b/src/hooks/usePrompts.ts @@ -1,6 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; import { useApiRequest } from './useApi'; -import { getUserDataCache } from '../utils/userCache'; import api from '../api'; import { usePermissions, type UserPermissions } from './usePermissions'; import { @@ -507,13 +506,9 @@ export function usePromptOperations() { setCreatingPrompt(true); try { - // Get mandateId from currentUser in sessionStorage cache - const currentUserData = getUserDataCache(); - const mandateId = currentUserData?.mandateId || ''; - - // Structure the request body as required by the API + // mandateId wird nicht mehr vom Client gesendet + // Das Backend bestimmt den Kontext über die instanceId im Request const requestBody = { - mandateId: mandateId, name: promptData.name, content: promptData.content }; @@ -533,13 +528,8 @@ export function usePromptOperations() { setUpdateError(null); try { - // Get mandateId from currentUser in sessionStorage cache - const currentUserData = getUserDataCache(); - const mandateId = currentUserData?.mandateId || ''; - - // Structure the request body as required by the API + // mandateId wird nicht mehr vom Client gesendet const requestBody = { - mandateId: mandateId, name: updateData.name, content: updateData.content }; diff --git a/src/hooks/useRoles.ts b/src/hooks/useRoles.ts new file mode 100644 index 0000000..014778e --- /dev/null +++ b/src/hooks/useRoles.ts @@ -0,0 +1,235 @@ +/** + * useRoles Hook + * + * Hook für die Verwaltung von globalen RBAC-Rollen im Admin-Bereich. + * Folgt dem gleichen Pattern wie useOrgUsers. + */ + +import { useState, useEffect, useCallback } from 'react'; +import { useApiRequest } from './useApi'; +import api from '../api'; +import { usePermissions, type UserPermissions } from './usePermissions'; +import { + fetchRoles as fetchRolesApi, + fetchRoleById as fetchRoleByIdApi, + createRole as createRoleApi, + updateRole as updateRoleApi, + deleteRole as deleteRoleApi, + type Role, + type RoleUpdateData, + type PaginationParams +} from '../api/roleApi'; + +// Re-export types +export type { Role, RoleUpdateData, PaginationParams }; + +export interface AttributeDefinition { + name: string; + type: string; + label: string; + description?: string; + required?: boolean; + default?: any; + options?: Array<{ value: string | number; label: string | { [key: string]: string } }> | string; + sortable?: boolean; + filterable?: boolean; + searchable?: boolean; + width?: number; + minWidth?: number; + maxWidth?: number; + readonly?: boolean; + editable?: boolean; +} + +/** + * Hook for managing RBAC roles in admin panel + */ +export function useAdminRoles() { + const [roles, setRoles] = useState([]); + const [attributes, setAttributes] = useState([]); + const [permissions, setPermissions] = useState(null); + const [pagination, setPagination] = useState<{ + currentPage: number; + pageSize: number; + totalItems: number; + totalPages: number; + } | null>(null); + const { request, isLoading: loading, error } = useApiRequest(); + const { checkPermission } = usePermissions(); + + // Fetch attributes from backend + const fetchAttributes = useCallback(async () => { + try { + const response = await api.get('/api/attributes/Role'); + + let attrs: AttributeDefinition[] = []; + if (response.data?.attributes && Array.isArray(response.data.attributes)) { + attrs = response.data.attributes; + } else if (Array.isArray(response.data)) { + attrs = response.data; + } else if (response.data && typeof response.data === 'object') { + const keys = Object.keys(response.data); + for (const key of keys) { + if (Array.isArray(response.data[key])) { + attrs = response.data[key]; + break; + } + } + } + + setAttributes(attrs); + return attrs; + } catch (error: any) { + if (error.response?.status === 429) { + console.warn('Rate limit exceeded while fetching role attributes.'); + } else if (error.response?.status !== 401) { + console.error('Error fetching role attributes:', error); + } + setAttributes([]); + return []; + } + }, []); + + // Fetch permissions + const fetchPermissions = useCallback(async () => { + try { + const perms = await checkPermission('DATA', 'Role'); + setPermissions(perms); + return perms; + } catch (error: any) { + console.error('Error fetching role permissions:', error); + const defaultPerms: UserPermissions = { + view: false, + read: 'n', + create: 'n', + update: 'n', + delete: 'n', + }; + setPermissions(defaultPerms); + return defaultPerms; + } + }, [checkPermission]); + + // Fetch roles + const fetchRoles = useCallback(async (params?: PaginationParams) => { + try { + const data = await fetchRolesApi(request, params); + + if (data && typeof data === 'object' && 'items' in data) { + const items = Array.isArray(data.items) ? data.items : []; + setRoles(items); + if (data.pagination) { + setPagination(data.pagination); + } + } else { + const items = Array.isArray(data) ? data : []; + setRoles(items); + setPagination(null); + } + } catch (error: any) { + setRoles([]); + setPagination(null); + } + }, [request]); + + // Optimistic updates + const removeOptimistically = (roleId: string) => { + setRoles(prev => prev.filter(r => r.id !== roleId)); + }; + + const updateOptimistically = (roleId: string, updateData: Partial) => { + setRoles(prev => + prev.map(r => r.id === roleId ? { ...r, ...updateData } : r) + ); + }; + + // Fetch single role + const fetchRoleById = useCallback(async (roleId: string): Promise => { + return await fetchRoleByIdApi(request, roleId); + }, [request]); + + // Generate columns from attributes + const columns = attributes.map(attr => ({ + key: attr.name, + label: attr.label || attr.name, + type: attr.type as any, + sortable: attr.sortable !== false, + filterable: attr.filterable !== false, + searchable: attr.searchable !== false, + width: attr.width || 150, + minWidth: attr.minWidth || 100, + maxWidth: attr.maxWidth || 400, + })); + + // Create role + const handleCreate = useCallback(async (roleData: Partial): Promise => { + try { + await createRoleApi(request, roleData); + await fetchRoles(); + return true; + } catch (error: any) { + console.error('Error creating role:', error); + return false; + } + }, [request, fetchRoles]); + + // Update role + const handleUpdate = useCallback(async (roleId: string, updateData: RoleUpdateData): Promise => { + try { + updateOptimistically(roleId, updateData); + await updateRoleApi(request, roleId, updateData); + return true; + } catch (error: any) { + console.error('Error updating role:', error); + await fetchRoles(); + return false; + } + }, [request, fetchRoles]); + + // Delete role + const handleDelete = useCallback(async (roleId: string): Promise => { + try { + removeOptimistically(roleId); + await deleteRoleApi(request, roleId); + return true; + } catch (error: any) { + console.error('Error deleting role:', error); + await fetchRoles(); + return false; + } + }, [request, fetchRoles]); + + // Inline update + const handleInlineUpdate = useCallback(async ( + roleId: string, + updateData: Partial + ): Promise => { + await handleUpdate(roleId, updateData); + }, [handleUpdate]); + + // Load data on mount + useEffect(() => { + fetchAttributes(); + fetchPermissions(); + fetchRoles(); + }, []); + + return { + roles, + attributes, + columns, + permissions, + pagination, + loading, + error, + refetch: fetchRoles, + fetchRoleById, + handleCreate, + handleUpdate, + handleDelete, + handleInlineUpdate, + updateOptimistically, + }; +} + +export default useAdminRoles; diff --git a/src/hooks/useTrustee.ts b/src/hooks/useTrustee.ts index 490d69b..55f1aea 100644 --- a/src/hooks/useTrustee.ts +++ b/src/hooks/useTrustee.ts @@ -1,7 +1,15 @@ +/** + * Trustee Hooks + * + * Hooks für das Trustee-Feature mit Instanz-Kontext. + * Die instanceId wird automatisch aus der URL gelesen. + */ + import { useState, useEffect, useCallback } from 'react'; import { useApiRequest } from './useApi'; import api from '../api'; import { usePermissions, type UserPermissions } from './usePermissions'; +import { useInstanceId } from './useCurrentInstance'; import { // Types type TrusteeOrganisation, @@ -94,15 +102,18 @@ export interface AttributeDefinition { interface TrusteeEntityConfig { entityName: string; - fetchAll: (request: any, params?: PaginationParams) => Promise; - fetchById: (request: any, id: string) => Promise; - create: (request: any, data: Partial) => Promise; - update: (request: any, id: string, data: Partial) => Promise; - deleteItem: (request: any, id: string) => Promise; + fetchAll: (request: any, instanceId: string, params?: PaginationParams) => Promise; + fetchById: (request: any, instanceId: string, id: string) => Promise; + create: (request: any, instanceId: string, data: Partial) => Promise; + update: (request: any, instanceId: string, id: string, data: Partial) => Promise; + deleteItem: (request: any, instanceId: string, id: string) => Promise; } function _createTrusteeEntityHook(config: TrusteeEntityConfig) { return function useTrusteeEntity() { + // Hole instanceId aus URL-Kontext + const instanceId = useInstanceId(); + const [items, setItems] = useState([]); const [attributes, setAttributes] = useState([]); const [permissions, setPermissions] = useState(null); @@ -116,8 +127,10 @@ function _createTrusteeEntityHook(config: TrusteeEntit const { checkPermission } = usePermissions(); const fetchAttributes = useCallback(async () => { + if (!instanceId) return []; + try { - const response = await api.get(`/api/attributes/${config.entityName}`); + const response = await api.get(`/api/trustee/${instanceId}/attributes/${config.entityName}`); let attrs: AttributeDefinition[] = []; if (response.data?.attributes && Array.isArray(response.data.attributes)) { attrs = response.data.attributes; @@ -131,7 +144,7 @@ function _createTrusteeEntityHook(config: TrusteeEntit setAttributes([]); return []; } - }, []); + }, [instanceId]); const fetchPermissions = useCallback(async () => { try { @@ -153,8 +166,13 @@ function _createTrusteeEntityHook(config: TrusteeEntit }, [checkPermission]); const fetchItems = useCallback(async (params?: PaginationParams) => { + if (!instanceId) { + setItems([]); + return; + } + try { - const data = await config.fetchAll(request, params); + const data = await config.fetchAll(request, instanceId, params); if (data && typeof data === 'object' && 'items' in data) { const fetchedItems = Array.isArray(data.items) ? data.items : []; @@ -171,7 +189,7 @@ function _createTrusteeEntityHook(config: TrusteeEntit setItems([]); setPagination(null); } - }, [request]); + }, [request, instanceId]); const removeOptimistically = (itemId: string) => { setItems(prev => prev.filter(item => item.id !== itemId)); @@ -188,8 +206,9 @@ function _createTrusteeEntityHook(config: TrusteeEntit }; const fetchById = useCallback(async (itemId: string): Promise => { - return await config.fetchById(request, itemId); - }, [request]); + if (!instanceId) return null; + return await config.fetchById(request, instanceId, itemId); + }, [request, instanceId]); const generateEditFieldsFromAttributes = useCallback(() => { if (!attributes || attributes.length === 0) { @@ -198,11 +217,9 @@ function _createTrusteeEntityHook(config: TrusteeEntit return attributes .filter(attr => { - // For EDIT mode: filter out readonly fields and system fields if (attr.readonly === true || attr.editable === false) { return false; } - // Also filter out 'id' for edit mode (id cannot be changed) if (attr.name === 'id') { return false; } @@ -265,7 +282,6 @@ function _createTrusteeEntityHook(config: TrusteeEntit }); }, [attributes]); - // Generate fields for CREATE forms - includes all required fields like 'id' const generateCreateFieldsFromAttributes = useCallback(() => { if (!attributes || attributes.length === 0) { return []; @@ -273,8 +289,6 @@ function _createTrusteeEntityHook(config: TrusteeEntit return attributes .filter(attr => { - // For CREATE mode: include all user-editable fields including 'id' - // Only filter out system-generated fields const systemFields = ['_createdBy', '_createdAt', '_modifiedBy', '_modifiedAt', 'mandateId']; return !systemFields.includes(attr.name); }) @@ -325,7 +339,7 @@ function _createTrusteeEntityHook(config: TrusteeEntit key: attr.name, label: attr.label || attr.name, type: fieldType, - editable: true, // All fields are editable in create mode + editable: true, required: attr.required === true, options, optionsReference, @@ -341,14 +355,14 @@ function _createTrusteeEntityHook(config: TrusteeEntit return await fetchAttributes(); }, [attributes, fetchAttributes]); + // Lade Daten wenn instanceId verfügbar useEffect(() => { - fetchAttributes(); - fetchPermissions(); - }, [fetchAttributes, fetchPermissions]); - - useEffect(() => { - fetchItems(); - }, [fetchItems]); + if (instanceId) { + fetchAttributes(); + fetchPermissions(); + fetchItems(); + } + }, [instanceId, fetchAttributes, fetchPermissions, fetchItems]); return { items, @@ -363,13 +377,17 @@ function _createTrusteeEntityHook(config: TrusteeEntit fetchById, generateEditFieldsFromAttributes, generateCreateFieldsFromAttributes, - ensureAttributesLoaded + ensureAttributesLoaded, + instanceId // Auch instanceId zurückgeben für Operations-Hook }; }; } function _createTrusteeOperationsHook(config: TrusteeEntityConfig) { return function useTrusteeEntityOperations() { + // Hole instanceId aus URL-Kontext + const instanceId = useInstanceId(); + const [deletingItems, setDeletingItems] = useState>(new Set()); const [creatingItem, setCreatingItem] = useState(false); const { request, isLoading } = useApiRequest(); @@ -377,12 +395,17 @@ function _createTrusteeOperationsHook(config: TrusteeE const [createError, setCreateError] = useState(null); const [updateError, setUpdateError] = useState(null); - const handleDelete = async (itemId: string) => { + const handleDelete = useCallback(async (itemId: string) => { + if (!instanceId) { + setDeleteError('No instance context'); + return false; + } + setDeleteError(null); setDeletingItems(prev => new Set(prev).add(itemId)); try { - await config.deleteItem(request, itemId); + await config.deleteItem(request, instanceId, itemId); await new Promise(resolve => setTimeout(resolve, 300)); return true; } catch (error: any) { @@ -395,20 +418,23 @@ function _createTrusteeOperationsHook(config: TrusteeE return newSet; }); } - }; + }, [request, instanceId]); - const handleCreate = async (itemData: Partial) => { + const handleCreate = useCallback(async (itemData: Partial) => { + if (!instanceId) { + setCreateError('No instance context'); + return { success: false, error: 'No instance context' }; + } + setCreateError(null); setCreatingItem(true); - // Debug: Log what data is being sent to the backend console.warn('🔧 handleCreate called with itemData:', itemData); try { - const newItem = await config.create(request, itemData); + const newItem = await config.create(request, instanceId, itemData); return { success: true, data: newItem }; } catch (error: any) { - // Debug: Log full error details console.error('🔧 handleCreate error:', { message: error.message, response: error.response?.data, @@ -420,13 +446,18 @@ function _createTrusteeOperationsHook(config: TrusteeE } finally { setCreatingItem(false); } - }; + }, [request, instanceId]); - const handleUpdate = async (itemId: string, updateData: Partial) => { + const handleUpdate = useCallback(async (itemId: string, updateData: Partial) => { + if (!instanceId) { + setUpdateError('No instance context'); + return { success: false, error: 'No instance context' }; + } + setUpdateError(null); try { - const updatedItem = await config.update(request, itemId, updateData); + const updatedItem = await config.update(request, instanceId, itemId, updateData); return { success: true, data: updatedItem }; } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || 'Failed to update'; @@ -439,7 +470,7 @@ function _createTrusteeOperationsHook(config: TrusteeE isValidationError: error.response?.status === 400 }; } - }; + }, [request, instanceId]); return { deletingItems, @@ -450,7 +481,8 @@ function _createTrusteeOperationsHook(config: TrusteeE handleDelete, handleCreate, handleUpdate, - isLoading + isLoading, + instanceId }; }; } @@ -558,7 +590,7 @@ export const useTrusteePositionOperations = _createTrusteeOperationsHook(positio const positionDocumentConfig: TrusteeEntityConfig = { entityName: 'TrusteePositionDocument', fetchAll: fetchPositionDocumentsApi, - fetchById: async () => null, // Not typically needed + fetchById: async () => null, create: createPositionDocumentApi, update: async () => { throw new Error('Update not supported for position-document links'); }, deleteItem: deletePositionDocumentApi diff --git a/src/hooks/useUsers.ts b/src/hooks/useUsers.ts index 263bba7..91823dc 100644 --- a/src/hooks/useUsers.ts +++ b/src/hooks/useUsers.ts @@ -31,25 +31,14 @@ export function useCurrentUser() { // Check if we already have user data in sessionStorage cache const cachedUser = getUserDataCache(); if (cachedUser && cachedUser.username) { - // Check if cached user has roleLabels - if empty, refetch from API - const hasRoleLabels = Array.isArray(cachedUser.roleLabels) && cachedUser.roleLabels.length > 0; - - if (!hasRoleLabels) { - console.warn('⚠️ Cached user data has no roleLabels, refetching from API:', { - username: cachedUser.username, - roleLabels: cachedUser.roleLabels - }); - // Clear cache and continue to fetch from API - clearUserDataCache(); - } else { - // Use cached user data - permissions are checked via RBAC API, not client-side - setUser(cachedUser); - console.log('✅ Using cached user data from sessionStorage (persists during session):', { - username: cachedUser.username, - roleLabels: cachedUser.roleLabels - }); - return; - } + // Use cached user data - permissions are checked via RBAC API, not client-side + // Note: roleLabels is deprecated in Multi-Tenant architecture - use isSysAdmin flag instead + setUser(cachedUser); + console.log('✅ Using cached user data from sessionStorage (persists during session):', { + username: cachedUser.username, + isSysAdmin: cachedUser.isSysAdmin + }); + return; } // JWT tokens are now stored in httpOnly cookies, so we fetch user data from API @@ -79,49 +68,28 @@ export function useCurrentUser() { const data = await fetchCurrentUserApi(request, authAuthority || undefined); - // Log full response for debugging + // Log response for debugging console.log('📦 User data received from API:', { username: data?.username, - roleLabels: data?.roleLabels, - hasRoleLabels: !!data?.roleLabels, - roleLabelsLength: Array.isArray(data?.roleLabels) ? data.roleLabels.length : 0, - roleLabelsContent: Array.isArray(data?.roleLabels) ? data.roleLabels : 'not an array', - allKeys: data ? Object.keys(data) : [], - fullData: JSON.stringify(data, null, 2) + isSysAdmin: data?.isSysAdmin, + allKeys: data ? Object.keys(data) : [] }); - // Always cache user data - permissions are checked via RBAC API, not client-side - // roleLabels are optional metadata for display/logging purposes + // Validate user data if (!data || !data.username) { console.error('❌ User data from API is invalid:', { username: data?.username, - dataKeys: data ? Object.keys(data) : [], - fullResponse: data + dataKeys: data ? Object.keys(data) : [] }); throw new Error('Invalid user data received from API'); } - // Check if API returned roleLabels - if not, log warning but still cache - const hasRoleLabels = Array.isArray(data.roleLabels) && data.roleLabels.length > 0; - - if (!hasRoleLabels) { - console.warn('⚠️ User data from API has no roleLabels - this may cause RBAC issues:', { - username: data.username, - roleLabels: data.roleLabels, - allKeys: Object.keys(data), - fullResponse: JSON.stringify(data, null, 2) - }); - // Still cache it, but log the issue - backend RBAC should handle permissions - // However, if backend expects roleLabels, this will cause problems - } - // Cache user data (permissions are checked via RBAC API) + // Note: roleLabels is deprecated - use isSysAdmin flag for admin checks setUserDataCache(data); - console.log('✅ User data fetched from API and cached in sessionStorage (secure):', { + console.log('✅ User data fetched from API and cached:', { username: data.username, - roleLabels: data.roleLabels, - roleLabelsLength: Array.isArray(data.roleLabels) ? data.roleLabels.length : 0, - hasRoleLabels + isSysAdmin: data.isSysAdmin }); setUser(data); } catch (error: any) { @@ -292,25 +260,12 @@ export function useCurrentUser() { // Try to load user from sessionStorage cache first for faster initial load const cachedUser = getUserDataCache(); if (cachedUser && cachedUser.username) { - // Check if cached user has roleLabels - if empty, refetch from API - const hasRoleLabels = Array.isArray(cachedUser.roleLabels) && cachedUser.roleLabels.length > 0; - - if (!hasRoleLabels) { - console.warn('⚠️ Cached user data has no roleLabels, refetching from API:', { - username: cachedUser.username, - roleLabels: cachedUser.roleLabels - }); - // Clear cache and refetch - clearUserDataCache(); - fetchCurrentUser(); - return; - } - // Use cached user data - permissions are checked via RBAC API + // Note: roleLabels is deprecated in Multi-Tenant architecture - use isSysAdmin flag instead setUser(cachedUser); - console.log('✅ Using cached user data from sessionStorage on mount (persists during session):', { + console.log('✅ Using cached user data from sessionStorage on mount:', { username: cachedUser.username, - roleLabels: cachedUser.roleLabels + isSysAdmin: cachedUser.isSysAdmin }); } @@ -835,20 +790,14 @@ export function useUserOperations() { } }; - const handleUserCreate = async (userData: Omit) => { + const handleUserCreate = async (userData: Omit) => { setCreateError(null); setCreatingUser(true); try { - const currentUserData = getUserDataCache(); - const mandateId = currentUserData?.mandateId || ''; - - const requestBody = { - mandateId: mandateId, - ...userData - }; - - const newUser = await createUserApi(request, requestBody); + // mandateId wird nicht mehr vom Client gesendet + // Das Backend bestimmt den Kontext über die instanceId im Request + const newUser = await createUserApi(request, userData); return { success: true, userData: newUser }; } catch (error: any) { @@ -889,15 +838,8 @@ export function useUserOperations() { setEditingUsers(prev => new Set(prev).add(userId)); try { - const currentUserData = getUserDataCache(); - const mandateId = currentUserData?.mandateId || ''; - - const requestBody = { - mandateId: mandateId, - ...updateData - }; - - const updatedUser = await updateUserApi(request, userId, requestBody); + // mandateId wird nicht mehr vom Client gesendet + const updatedUser = await updateUserApi(request, userId, updateData); return { success: true, userData: updatedUser }; } catch (error: any) { diff --git a/src/index.css b/src/index.css index 34d6aee..c07a664 100644 --- a/src/index.css +++ b/src/index.css @@ -8,6 +8,7 @@ html, body { padding: 0; height: 100%; overflow: hidden; + font-family: var(--font-family, "DM Sans", sans-serif); } #root { @@ -15,4 +16,5 @@ html, body { width: 100vw; margin: 0; padding: 0; + font-family: var(--font-family, "DM Sans", sans-serif); } \ No newline at end of file diff --git a/src/layouts/MainLayout.module.css b/src/layouts/MainLayout.module.css index 63c802b..35f7dd9 100644 --- a/src/layouts/MainLayout.module.css +++ b/src/layouts/MainLayout.module.css @@ -31,6 +31,12 @@ border-bottom: 1px solid var(--border-color, #e0e0e0); } +.logoImage { + height: 40px; + width: auto; + object-fit: contain; +} + .logoText { font-size: 1.5rem; font-weight: 700; @@ -93,6 +99,10 @@ border-bottom-color: var(--border-dark, #333); } +:global(.dark-theme) .logoImage { + filter: brightness(0) invert(1); +} + :global(.dark-theme) .logoPower { color: var(--text-primary-dark, #ffffff); } diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx index 42fdbe6..c19321b 100644 --- a/src/layouts/MainLayout.tsx +++ b/src/layouts/MainLayout.tsx @@ -9,6 +9,7 @@ import React, { useEffect } from 'react'; import { Outlet } from 'react-router-dom'; import { FeatureProvider, useFeatureStore } from '../stores/featureStore'; import { MandateNavigation } from '../components/Navigation/MandateNavigation'; +import { UserSection } from '../components/Navigation/UserSection'; import styles from './MainLayout.module.css'; // ============================================================================= @@ -30,10 +31,11 @@ const MainLayoutInner: React.FC = () => { {/* Sidebar */} {/* Content */} diff --git a/src/pages/FeatureView.module.css b/src/pages/FeatureView.module.css index 9bbc145..035a081 100644 --- a/src/pages/FeatureView.module.css +++ b/src/pages/FeatureView.module.css @@ -6,10 +6,11 @@ display: flex; flex-direction: column; height: 100%; + overflow: hidden; } -/* View Header */ .viewHeader { + flex-shrink: 0; padding: 1rem 1.5rem; border-bottom: 1px solid var(--border-color, #e0e0e0); background: var(--bg-primary, #ffffff); @@ -22,38 +23,33 @@ color: var(--text-primary, #1a1a1a); } -/* View Content */ .viewContent { flex: 1; overflow: auto; padding: 1.5rem; } -/* Placeholder */ +/* Placeholder View */ .placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; - min-height: 300px; - padding: 2rem; - background: var(--surface-color, #f8f9fa); - border: 2px dashed var(--border-color, #e0e0e0); - border-radius: 12px; + min-height: 400px; text-align: center; + padding: 2rem; } .placeholder h2 { - margin: 0; - font-size: 1.25rem; + margin: 0 0 0.5rem; + font-size: 1.5rem; font-weight: 600; color: var(--text-primary, #1a1a1a); } .placeholder p { - margin: 0.5rem 0 0; + margin: 0; color: var(--text-secondary, #666); - font-size: 0.9375rem; } /* Not Found */ @@ -63,29 +59,22 @@ flex-direction: column; align-items: center; justify-content: center; - min-height: 300px; - padding: 2rem; + min-height: 400px; text-align: center; + padding: 2rem; } .notFound h2, .accessDenied h2 { - margin: 0; + margin: 0 0 0.5rem; font-size: 1.25rem; font-weight: 600; - color: var(--text-primary, #1a1a1a); } .notFound p, .accessDenied p { - margin: 0.5rem 0 0; + margin: 0; color: var(--text-secondary, #666); - font-size: 0.9375rem; -} - -.accessDenied { - background: var(--error-light, #fef2f2); - border-radius: 12px; } .accessDenied h2 { @@ -98,25 +87,14 @@ border-bottom-color: var(--border-dark, #333); } -:global(.dark-theme) .viewTitle { - color: var(--text-primary-dark, #ffffff); -} - -:global(.dark-theme) .placeholder { - background: var(--surface-dark, #1a1a1a); - border-color: var(--border-dark, #444); -} - +:global(.dark-theme) .viewTitle, :global(.dark-theme) .placeholder h2, :global(.dark-theme) .notFound h2 { color: var(--text-primary-dark, #ffffff); } :global(.dark-theme) .placeholder p, -:global(.dark-theme) .notFound p { +:global(.dark-theme) .notFound p, +:global(.dark-theme) .accessDenied p { color: var(--text-secondary-dark, #aaa); } - -:global(.dark-theme) .accessDenied { - background: rgba(220, 38, 38, 0.1); -} diff --git a/src/pages/FeatureView.tsx b/src/pages/FeatureView.tsx index 99488d7..76780ee 100644 --- a/src/pages/FeatureView.tsx +++ b/src/pages/FeatureView.tsx @@ -3,105 +3,55 @@ * * Generische Feature-View-Komponente. * Rendert den entsprechenden Content basierend auf Feature-Code und View. - * - * Die Komponente ist Feature-agnostisch und delegiert an spezifische View-Komponenten. */ import React from 'react'; import { useCurrentInstance } from '../hooks/useCurrentInstance'; import { useCanViewFeatureView } from '../hooks/useInstancePermissions'; import { getLabel, FEATURE_REGISTRY } from '../types/mandate'; + +// Trustee Views +import { TrusteeContractsView } from './views/trustee/TrusteeContractsView'; +import { TrusteeOrganisationsView } from './views/trustee/TrusteeOrganisationsView'; +import { TrusteeDocumentsView } from './views/trustee/TrusteeDocumentsView'; +import { TrusteePositionsView } from './views/trustee/TrusteePositionsView'; +import { TrusteeRolesView } from './views/trustee/TrusteeRolesView'; +import { TrusteeAccessView } from './views/trustee/TrusteeAccessView'; +import { TrusteeDashboardView } from './views/trustee/TrusteeDashboardView'; + import styles from './FeatureView.module.css'; // ============================================================================= -// VIEW COMPONENTS (Placeholders - werden später durch echte ersetzt) +// PLACEHOLDER VIEWS (für nicht implementierte Features) // ============================================================================= -// Trustee Views -const TrusteeDashboard: React.FC = () => ( +const PlaceholderView: React.FC<{ title: string; description: string }> = ({ title, description }) => (
-

Trustee Dashboard

-

Übersicht der Treuhand-Aktivitäten

-
-); - -const TrusteeOrganisations: React.FC = () => ( -
-

Organisationen

-

Verwaltung der Organisationen

-
-); - -const TrusteeContracts: React.FC = () => ( -
-

Verträge

-

Vertragsverwaltung

-
-); - -const TrusteeDocuments: React.FC = () => ( -
-

Dokumente

-

Dokumentenverwaltung

-
-); - -const TrusteePositions: React.FC = () => ( -
-

Positionen

-

Positionsverwaltung

-
-); - -const TrusteeRoles: React.FC = () => ( -
-

Rollen

-

Rollenverwaltung

-
-); - -const TrusteeAccess: React.FC = () => ( -
-

Zugriffe

-

Zugriffsverwaltung

+

{title}

+

{description}

); // Chatworkflow Views const ChatworkflowDashboard: React.FC = () => ( -
-

Workflow Dashboard

-

Übersicht der Workflows

-
+ ); const ChatworkflowRuns: React.FC = () => ( -
-

Runs

-

Workflow-Ausführungen

-
+ ); const ChatworkflowFiles: React.FC = () => ( -
-

Dateien

-

Workflow-Dateien

-
+ ); // Chatbot Views const ChatbotConversations: React.FC = () => ( -
-

Konversationen

-

Chat-Konversationen

-
+ ); const ChatbotSettings: React.FC = () => ( -
-

Chatbot Einstellungen

-

Konfiguration des Chatbots

-
+ ); // Generic/Fallback @@ -127,13 +77,13 @@ type ViewComponent = React.FC; const VIEW_COMPONENTS: Record> = { trustee: { - dashboard: TrusteeDashboard, - organisations: TrusteeOrganisations, - contracts: TrusteeContracts, - documents: TrusteeDocuments, - positions: TrusteePositions, - roles: TrusteeRoles, - access: TrusteeAccess, + dashboard: TrusteeDashboardView, + organisations: TrusteeOrganisationsView, + contracts: TrusteeContractsView, + documents: TrusteeDocumentsView, + positions: TrusteePositionsView, + roles: TrusteeRolesView, + access: TrusteeAccessView, }, chatworkflow: { dashboard: ChatworkflowDashboard, diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index ad1923d..ff4b1cf 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -13,7 +13,7 @@ import styles from './Settings.module.css'; // ============================================================================= export const SettingsPage: React.FC = () => { - const { t, language, setLanguage } = useLanguage(); + const { currentLanguage, setLanguage } = useLanguage(); const [theme, setTheme] = useState<'light' | 'dark'>( () => (localStorage.getItem('theme') as 'light' | 'dark') || 'light' ); @@ -79,7 +79,7 @@ export const SettingsPage: React.FC = () => {