revised ui components

This commit is contained in:
ValueOn AG 2026-01-20 00:56:00 +01:00
parent 8033ca9207
commit 70c84dd897
54 changed files with 5159 additions and 600 deletions

View file

@ -2,9 +2,13 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%- VITE_APP_NAME %></title> <title><%- VITE_APP_NAME %></title>
<!-- Google Fonts - DM Sans -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap" rel="stylesheet">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="60" fill="none" xmlns:v="https://vecta.io/nano"><g clip-path="url(#A)" fill="#fffefd"><path d="M9.001 28.103c-3.277-.84-4.094-1.246-4.094-2.492v-.055c0-.923.84-1.657 2.442-1.657s3.249.706 4.929 1.869l2.169-3.143c-1.92-1.546-4.278-2.414-7.043-2.414-3.877 0-6.637 2.275-6.637 5.718v.055c0 3.766 2.465 4.823 6.286 5.802 3.171.812 3.822 1.357 3.822 2.414v.055c0 1.112-1.029 1.791-2.737 1.791-2.169 0-3.955-.895-5.663-2.303L.01 36.697c2.275 2.031 5.174 3.032 8.049 3.032 4.094 0 6.965-2.114 6.965-5.88v-.055c0-3.305-2.169-4.689-6.018-5.691h-.005zm22.582-.923c0 1.625-1.218 2.871-3.305 2.871h-3.309v-5.797h3.226c2.086 0 3.388 1.002 3.388 2.871v.055zm-3.037-6.692h-7.749v18.969h4.172v-5.691h3.171c4.251 0 7.671-2.275 7.671-6.665v-.055c0-3.877-2.737-6.558-7.265-6.558zm16.939 0h-4.172v18.969h4.172V20.488zm4.384 3.664v.18h5.769v15.125h4.172V24.332h5.774v-3.845H49.869v3.665zm27.794 11.783c-3.254 0-5.501-2.709-5.501-5.963v-.055c0-3.249 2.303-5.908 5.501-5.908 1.897 0 3.388.812 4.846 2.142l2.654-3.06c-1.758-1.735-3.905-2.926-7.477-2.926-5.829 0-9.891 4.417-9.891 9.808v.055c0 5.446 4.145 9.757 9.729 9.757 3.66 0 5.825-1.301 7.777-3.388l-2.654-2.681c-1.491 1.352-2.82 2.22-4.985 2.22zm23.746-15.447v7.509h-7.694v-7.509h-4.172v18.969h4.172v-7.615h7.694v7.615h4.172V20.488h-4.172zM56.995 0h-8.571l4.288 7.615L56.995 0zm-8.571 60h8.571l-4.283-7.615L48.424 60z"/></g><defs><clipPath id="A"><path fill="#fff" d="M0 0h105.582v60H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

192
public/poweron-home.html Normal file
View file

@ -0,0 +1,192 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="google-site-verification" content="HF3EVKLJvD7jp5xiS-r7in7Jo01_okijtWzDSnu_YhQ" />
<title>PowerOn AI Platform - Home</title>
<link rel="icon" type="image/x-icon" href="./favicon.ico">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8fafc;
color: #1e293b;
line-height: 1.6;
font-size: 16px;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background-color: #ffffff;
min-height: 100vh;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 2px solid #e2e8f0;
}
.header h1 {
color: #3b82f6;
font-size: 2.5rem;
margin-bottom: 1rem;
}
.header p {
color: #64748b;
font-size: 1.1rem;
}
.content-section {
margin-bottom: 2.5rem;
}
.content-section h2 {
color: #1e293b;
font-size: 1.5rem;
margin-bottom: 1rem;
border-left: 4px solid #3b82f6;
padding-left: 1rem;
}
.content-section p {
margin-bottom: 1rem;
color: #475569;
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin: 2rem 0;
}
.feature-card {
background-color: #f1f5f9;
padding: 1.5rem;
border-radius: 0.5rem;
border-left: 4px solid #3b82f6;
}
.feature-card h3 {
color: #1e293b;
margin-bottom: 0.5rem;
}
.feature-card p {
color: #64748b;
font-size: 0.95rem;
}
.navigation {
text-align: center;
margin-top: 3rem;
padding-top: 2rem;
border-top: 2px solid #e2e8f0;
}
.nav-link {
display: inline-block;
margin: 0 1rem;
padding: 0.75rem 1.5rem;
background-color: #3b82f6;
color: white;
text-decoration: none;
border-radius: 0.25rem;
transition: background-color 0.2s ease;
}
.nav-link:hover {
background-color: #2563eb;
}
.footer {
text-align: center;
margin-top: 2rem;
color: #64748b;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>PowerOn AI Platform</h1>
<p>Intelligent Workflow Automation & Multi-Agent Collaboration</p>
</div>
<div class="content-section">
<h2>What is PowerOn?</h2>
<p>
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.
</p>
</div>
<div class="content-section">
<h2>Core Capabilities</h2>
<div class="features">
<div class="feature-card">
<h3>AI Agent Management</h3>
<p>Create, configure, and manage multiple AI agents for different business tasks and workflows.</p>
</div>
<div class="feature-card">
<h3>Workflow Automation</h3>
<p>Design and execute complex business processes with drag-and-drop workflow builder.</p>
</div>
<div class="feature-card">
<h3>Document Processing</h3>
<p>Intelligent document extraction, analysis, and generation powered by AI.</p>
</div>
<div class="feature-card">
<h3>Multi-Platform Integration</h3>
<p>Seamlessly connect with Microsoft 365, SharePoint, Outlook, and web services.</p>
</div>
</div>
</div>
<div class="content-section">
<h2>Who Benefits from PowerOn?</h2>
<p>
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.
</p>
</div>
<div class="content-section">
<h2>Key Benefits</h2>
<ul style="color: #475569; margin-left: 2rem;">
<li>Reduce manual work by up to 80% through intelligent automation</li>
<li>Improve accuracy and consistency in business processes</li>
<li>Enable 24/7 operation with AI agents that never sleep</li>
<li>Scale operations without proportional increase in human resources</li>
<li>Gain insights from AI-powered analytics and reporting</li>
</ul>
</div>
<div class="navigation">
<a href="poweron-privacy.html" class="nav-link">Privacy Policy</a>
<a href="poweron-terms.html" class="nav-link">Terms of Service</a>
</div>
<div class="footer">
<p>&copy; 2025 PowerOn AI Platform. All rights reserved.</p>
</div>
</div>
</body>
</html>

290
public/poweron-privacy.html Normal file
View file

@ -0,0 +1,290 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PowerOn AI Platform - Privacy Policy</title>
<link rel="icon" type="image/x-icon" href="./favicon.ico">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8fafc;
color: #1e293b;
line-height: 1.6;
font-size: 16px;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background-color: #ffffff;
min-height: 100vh;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 2px solid #e2e8f0;
}
.header h1 {
color: #3b82f6;
font-size: 2.5rem;
margin-bottom: 1rem;
}
.header p {
color: #64748b;
font-size: 1.1rem;
}
.content-section {
margin-bottom: 2.5rem;
}
.content-section h2 {
color: #1e293b;
font-size: 1.5rem;
margin-bottom: 1rem;
border-left: 4px solid #3b82f6;
padding-left: 1rem;
}
.content-section h3 {
color: #1e293b;
font-size: 1.2rem;
margin: 1.5rem 0 0.75rem 0;
}
.content-section p {
margin-bottom: 1rem;
color: #475569;
}
.content-section ul {
margin-left: 2rem;
margin-bottom: 1rem;
}
.content-section li {
color: #475569;
margin-bottom: 0.5rem;
}
.highlight-box {
background-color: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 1.5rem;
margin: 1.5rem 0;
}
.highlight-box h4 {
color: #1e293b;
margin-bottom: 0.75rem;
}
.navigation {
text-align: center;
margin-top: 3rem;
padding-top: 2rem;
border-top: 2px solid #e2e8f0;
}
.nav-link {
display: inline-block;
margin: 0 1rem;
padding: 0.75rem 1.5rem;
background-color: #3b82f6;
color: white;
text-decoration: none;
border-radius: 0.25rem;
transition: background-color 0.2s ease;
}
.nav-link:hover {
background-color: #2563eb;
}
.footer {
text-align: center;
margin-top: 2rem;
color: #64748b;
font-size: 0.9rem;
}
.last-updated {
background-color: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 2rem;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Privacy Policy</h1>
<p>PowerOn AI Platform - Data Protection & Privacy</p>
</div>
<div class="last-updated">
<strong>Last Updated:</strong> August 2025
</div>
<div class="content-section">
<h2>Introduction</h2>
<p>
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.
</p>
</div>
<div class="content-section">
<h2>Information We Collect</h2>
<h3>Personal Information</h3>
<p>We may collect the following types of personal information:</p>
<ul>
<li>Name and contact information (email address, phone number)</li>
<li>Company and job title information</li>
<li>Authentication credentials and account settings</li>
<li>Payment and billing information</li>
</ul>
<h3>Usage Information</h3>
<p>We automatically collect information about how you use our platform:</p>
<ul>
<li>Workflow creation and execution data</li>
<li>AI agent interactions and configurations</li>
<li>Document processing activities</li>
<li>Platform access logs and performance metrics</li>
</ul>
<h3>Technical Information</h3>
<p>We collect technical information to ensure platform functionality:</p>
<ul>
<li>Device and browser information</li>
<li>IP address and location data</li>
<li>Cookies and similar tracking technologies</li>
<li>System performance and error logs</li>
</ul>
</div>
<div class="content-section">
<h2>How We Use Your Information</h2>
<p>We use the collected information for the following purposes:</p>
<ul>
<li>Provide and maintain our AI platform services</li>
<li>Process and execute your workflow automations</li>
<li>Improve platform performance and user experience</li>
<li>Send important service updates and notifications</li>
<li>Provide customer support and technical assistance</li>
<li>Ensure platform security and prevent fraud</li>
<li>Comply with legal obligations and regulations</li>
</ul>
</div>
<div class="content-section">
<h2>Data Sharing and Disclosure</h2>
<p>We do not sell, trade, or rent your personal information to third parties. We may share your information only in the following circumstances:</p>
<div class="highlight-box">
<h4>Service Providers</h4>
<p>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.</p>
</div>
<div class="highlight-box">
<h4>Legal Requirements</h4>
<p>We may disclose your information if required by law, court order, or government regulation, or to protect our rights, property, or safety.</p>
</div>
<div class="highlight-box">
<h4>Business Transfers</h4>
<p>In the event of a merger, acquisition, or sale of assets, your information may be transferred as part of the business transaction.</p>
</div>
</div>
<div class="content-section">
<h2>Data Security</h2>
<p>We implement comprehensive security measures to protect your information:</p>
<ul>
<li>Encryption of data in transit and at rest</li>
<li>Regular security audits and vulnerability assessments</li>
<li>Access controls and authentication mechanisms</li>
<li>Secure data centers and infrastructure</li>
<li>Employee training on data protection practices</li>
</ul>
</div>
<div class="content-section">
<h2>Your Rights and Choices</h2>
<p>You have the following rights regarding your personal information:</p>
<ul>
<li><strong>Access:</strong> Request a copy of your personal information</li>
<li><strong>Correction:</strong> Update or correct inaccurate information</li>
<li><strong>Deletion:</strong> Request deletion of your personal information</li>
<li><strong>Portability:</strong> Receive your data in a portable format</li>
<li><strong>Opt-out:</strong> Unsubscribe from marketing communications</li>
</ul>
</div>
<div class="content-section">
<h2>Data Retention</h2>
<p>We retain your personal information only as long as necessary to:</p>
<ul>
<li>Provide our services to you</li>
<li>Comply with legal obligations</li>
<li>Resolve disputes and enforce agreements</li>
<li>Improve our platform and services</li>
</ul>
<p>When we no longer need your information, we securely delete or anonymize it.</p>
</div>
<div class="content-section">
<h2>International Data Transfers</h2>
<p>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.</p>
</div>
<div class="content-section">
<h2>Children's Privacy</h2>
<p>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.</p>
</div>
<div class="content-section">
<h2>Changes to This Policy</h2>
<p>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.</p>
</div>
<div class="content-section">
<h2>Contact Us</h2>
<p>If you have any questions about this Privacy Policy or our data practices, please contact us:</p>
<div class="highlight-box">
<p><strong>Email:</strong> privacy@poweron-ai.com</p>
<p><strong>Address:</strong> PowerOn AI Platform, Privacy Team</p>
</div>
</div>
<div class="navigation">
<a href="poweron-home.html" class="nav-link">Home</a>
<a href="poweron-terms.html" class="nav-link">Terms of Service</a>
</div>
<div class="footer">
<p>&copy; 2025 PowerOn AI Platform. All rights reserved.</p>
</div>
</div>
</body>
</html>

333
public/poweron-terms.html Normal file
View file

@ -0,0 +1,333 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PowerOn AI Platform - Terms of Service</title>
<link rel="icon" type="image/x-icon" href="./favicon.ico">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f8fafc;
color: #1e293b;
line-height: 1.6;
font-size: 16px;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background-color: #ffffff;
min-height: 100vh;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 2px solid #e2e8f0;
}
.header h1 {
color: #3b82f6;
font-size: 2.5rem;
margin-bottom: 1rem;
}
.header p {
color: #64748b;
font-size: 1.1rem;
}
.content-section {
margin-bottom: 2.5rem;
}
.content-section h2 {
color: #1e293b;
font-size: 1.5rem;
margin-bottom: 1rem;
border-left: 4px solid #3b82f6;
padding-left: 1rem;
}
.content-section h3 {
color: #1e293b;
font-size: 1.2rem;
margin: 1.5rem 0 0.75rem 0;
}
.content-section p {
margin-bottom: 1rem;
color: #475569;
}
.content-section ul {
margin-left: 2rem;
margin-bottom: 1rem;
}
.content-section li {
color: #475569;
margin-bottom: 0.5rem;
}
.highlight-box {
background-color: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 1.5rem;
margin: 1.5rem 0;
}
.highlight-box h4 {
color: #1e293b;
margin-bottom: 0.75rem;
}
.warning-box {
background-color: #fef2f2;
border: 1px solid #fecaca;
border-radius: 0.5rem;
padding: 1.5rem;
margin: 1.5rem 0;
}
.warning-box h4 {
color: #dc2626;
margin-bottom: 0.75rem;
}
.navigation {
text-align: center;
margin-top: 3rem;
padding-top: 2rem;
border-top: 2px solid #e2e8f0;
}
.nav-link {
display: inline-block;
margin: 0 1rem;
padding: 0.75rem 1.5rem;
background-color: #3b82f6;
color: white;
text-decoration: none;
border-radius: 0.25rem;
transition: background-color 0.2s ease;
}
.nav-link:hover {
background-color: #2563eb;
}
.footer {
text-align: center;
margin-top: 2rem;
color: #64748b;
font-size: 0.9rem;
}
.last-updated {
background-color: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 2rem;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Terms of Service</h1>
<p>PowerOn AI Platform - Service Agreement & User Terms</p>
</div>
<div class="last-updated">
<strong>Last Updated:</strong> August 2025
</div>
<div class="content-section">
<h2>Acceptance of Terms</h2>
<p>
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").
</p>
</div>
<div class="content-section">
<h2>Description of Service</h2>
<p>
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.
</p>
<p>Our Platform includes the following services:</p>
<ul>
<li>AI agent creation and management</li>
<li>Workflow design and automation tools</li>
<li>Document processing and analysis capabilities</li>
<li>Integration with third-party services and platforms</li>
<li>Analytics and reporting features</li>
</ul>
</div>
<div class="content-section">
<h2>User Accounts and Registration</h2>
<h3>Account Creation</h3>
<p>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.</p>
<h3>Account Security</h3>
<p>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.</p>
<h3>Account Termination</h3>
<p>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.</p>
</div>
<div class="content-section">
<h2>Acceptable Use Policy</h2>
<p>You agree to use our Platform only for lawful purposes and in accordance with these Terms. You agree not to:</p>
<div class="warning-box">
<h4>Prohibited Activities</h4>
<ul>
<li>Use the Platform for any illegal or unauthorized purpose</li>
<li>Violate any applicable laws or regulations</li>
<li>Infringe upon the intellectual property rights of others</li>
<li>Attempt to gain unauthorized access to our systems</li>
<li>Interfere with or disrupt the Platform's operation</li>
<li>Use the Platform to transmit harmful or malicious code</li>
<li>Harass, abuse, or harm other users</li>
</ul>
</div>
</div>
<div class="content-section">
<h2>User Content and Data</h2>
<h3>Content Ownership</h3>
<p>You retain ownership of any content, data, or information you upload, create, or process through our Platform ("User Content").</p>
<h3>Content License</h3>
<p>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.</p>
<h3>Content Responsibility</h3>
<p>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.</p>
</div>
<div class="content-section">
<h2>Service Availability and Limitations</h2>
<div class="highlight-box">
<h4>Service Availability</h4>
<p>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.</p>
</div>
<div class="highlight-box">
<h4>Service Limitations</h4>
<p>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.</p>
</div>
</div>
<div class="content-section">
<h2>Intellectual Property Rights</h2>
<p>
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.
</p>
</div>
<div class="content-section">
<h2>Third-Party Services and Integrations</h2>
<p>
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.
</p>
</div>
<div class="content-section">
<h2>Payment Terms</h2>
<h3>Pricing and Billing</h3>
<p>Service pricing is available on our Platform and may be subject to change. We will provide reasonable notice of any price changes.</p>
<h3>Payment Obligations</h3>
<p>You agree to pay all fees associated with your use of our Platform. Failure to pay may result in service suspension or termination.</p>
<h3>Refunds</h3>
<p>Refund policies are determined by your subscription plan and are subject to our discretion and applicable laws.</p>
</div>
<div class="content-section">
<h2>Disclaimers and Limitations of Liability</h2>
<div class="warning-box">
<h4>Service Disclaimers</h4>
<p>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.</p>
</div>
<div class="warning-box">
<h4>Limitation of Liability</h4>
<p>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.</p>
</div>
</div>
<div class="content-section">
<h2>Indemnification</h2>
<p>
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.
</p>
</div>
<div class="content-section">
<h2>Governing Law and Dispute Resolution</h2>
<p>
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.
</p>
</div>
<div class="content-section">
<h2>Changes to Terms</h2>
<p>
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.
</p>
</div>
<div class="content-section">
<h2>Contact Information</h2>
<p>If you have any questions about these Terms of Service, please contact us:</p>
<div class="highlight-box">
<p><strong>Email:</strong> legal@poweron-ai.com</p>
<p><strong>Address:</strong> PowerOn AI Platform, Legal Department</p>
</div>
</div>
<div class="navigation">
<a href="poweron-home.html" class="nav-link">Home</a>
<a href="poweron-privacy.html" class="nav-link">Privacy Policy</a>
</div>
<div class="footer">
<p>&copy; 2025 PowerOn AI Platform. All rights reserved.</p>
</div>
</div>
</body>
</html>

View file

@ -35,6 +35,7 @@ import { FeatureLayout } from './layouts/FeatureLayout';
import { DashboardPage } from './pages/Dashboard'; import { DashboardPage } from './pages/Dashboard';
import { SettingsPage } from './pages/Settings'; import { SettingsPage } from './pages/Settings';
import { FeatureViewPage } from './pages/FeatureView'; import { FeatureViewPage } from './pages/FeatureView';
import { AdminMandatesPage, AdminUsersPage, AdminRolesPage } from './pages/admin';
function App() { function App() {
// Load saved theme preference and set app name on app mount // Load saved theme preference and set app name on app mount
@ -115,9 +116,9 @@ function App() {
{/* ADMIN ROUTES (nur SysAdmin) */} {/* ADMIN ROUTES (nur SysAdmin) */}
{/* ============================================== */} {/* ============================================== */}
<Route path="admin"> <Route path="admin">
<Route path="mandates" element={<div>Admin: Mandanten (TODO)</div>} /> <Route path="mandates" element={<AdminMandatesPage />} />
<Route path="users" element={<div>Admin: Benutzer (TODO)</div>} /> <Route path="users" element={<AdminUsersPage />} />
<Route path="roles" element={<div>Admin: Globale Rollen (TODO)</div>} /> <Route path="roles" element={<AdminRolesPage />} />
</Route> </Route>
</Route> </Route>

View file

@ -85,15 +85,18 @@ export interface UsernameAvailabilityResponse {
message: string; 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; id: string;
username: string; username: string;
email: string; email: string;
fullName: string; fullName: string;
language: string; language: string;
enabled: boolean; enabled: boolean;
privilege: string; roleLabels?: string[];
mandateId: string; authenticationAuthority: string;
isSysAdmin?: boolean;
[key: string]: any; [key: string]: any;
} }
@ -138,7 +141,7 @@ export async function loginApi(loginData: LoginRequest): Promise<LoginResponse>
* Fetch current user data * Fetch current user data
* Endpoint: GET /api/local/me | /api/msft/me | /api/google/me * Endpoint: GET /api/local/me | /api/msft/me | /api/google/me
*/ */
export async function fetchCurrentUserApi(authAuthority?: string): Promise<User> { export async function fetchCurrentUserApi(authAuthority?: string): Promise<AuthUser> {
let endpoint = '/api/local/me'; let endpoint = '/api/local/me';
if (authAuthority === 'msft') { if (authAuthority === 'msft') {
@ -147,7 +150,7 @@ export async function fetchCurrentUserApi(authAuthority?: string): Promise<User>
endpoint = '/api/google/me'; endpoint = '/api/google/me';
} }
const response = await api.get<User>(endpoint); const response = await api.get<AuthUser>(endpoint);
return response.data; return response.data;
} }

View file

@ -144,7 +144,7 @@ const MOCK_RESPONSE: FeaturesMyResponse = {
}; };
// Flag für Mock-Modus (auf false setzen wenn Backend bereit) // Flag für Mock-Modus (auf false setzen wenn Backend bereit)
const USE_MOCK = true; const USE_MOCK = false;
// ============================================================================= // =============================================================================
// API FUNCTIONS // API FUNCTIONS

View file

@ -62,15 +62,16 @@ export interface PaginatedResponse<T> {
} }
export interface CreatePromptData { export interface CreatePromptData {
mandateId: string;
name: string; name: string;
content: string; content: string;
// mandateId wird nicht mehr vom Client gesendet
// Das Backend bestimmt den Kontext über die instanceId
} }
export interface UpdatePromptData { export interface UpdatePromptData {
mandateId: string;
name: string; name: string;
content: string; content: string;
// mandateId wird nicht mehr vom Client gesendet
} }
// Type for the request function passed to API functions // Type for the request function passed to API functions

View file

@ -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'; import { ApiRequestOptions } from '../hooks/useApi';
// ============================================================================ // ============================================================================
@ -60,7 +69,7 @@ export interface TrusteeDocument {
contractId: string; contractId: string;
documentName: string; documentName: string;
documentMimeType: string; documentMimeType: string;
documentData?: any; // Binary data, typically not included in list responses documentData?: any;
mandateId?: string; mandateId?: string;
_createdAt?: number; _createdAt?: number;
_modifiedAt?: number; _modifiedAt?: number;
@ -150,16 +159,24 @@ function _buildPaginationParams(params?: PaginationParams): Record<string, any>
return requestParams; return requestParams;
} }
/**
* Erstellt die Basis-URL für Trustee-Endpunkte
*/
function _getTrusteeBaseUrl(instanceId: string): string {
return `/api/trustee/${instanceId}`;
}
// ============================================================================ // ============================================================================
// ORGANISATION API // ORGANISATION API
// ============================================================================ // ============================================================================
export async function fetchOrganisations( export async function fetchOrganisations(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
params?: PaginationParams params?: PaginationParams
): Promise<PaginatedResponse<TrusteeOrganisation> | TrusteeOrganisation[]> { ): Promise<PaginatedResponse<TrusteeOrganisation> | TrusteeOrganisation[]> {
return await request({ return await request({
url: '/api/trustee/organisations', url: `${_getTrusteeBaseUrl(instanceId)}/organisations`,
method: 'get', method: 'get',
params: _buildPaginationParams(params) params: _buildPaginationParams(params)
}); });
@ -167,11 +184,12 @@ export async function fetchOrganisations(
export async function fetchOrganisationById( export async function fetchOrganisationById(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
orgId: string orgId: string
): Promise<TrusteeOrganisation | null> { ): Promise<TrusteeOrganisation | null> {
try { try {
return await request({ return await request({
url: `/api/trustee/organisations/${orgId}`, url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`,
method: 'get' method: 'get'
}); });
} catch (error: any) { } catch (error: any) {
@ -182,10 +200,11 @@ export async function fetchOrganisationById(
export async function createOrganisation( export async function createOrganisation(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
data: Partial<TrusteeOrganisation> data: Partial<TrusteeOrganisation>
): Promise<TrusteeOrganisation> { ): Promise<TrusteeOrganisation> {
return await request({ return await request({
url: '/api/trustee/organisations', url: `${_getTrusteeBaseUrl(instanceId)}/organisations`,
method: 'post', method: 'post',
data data
}); });
@ -193,11 +212,12 @@ export async function createOrganisation(
export async function updateOrganisation( export async function updateOrganisation(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
orgId: string, orgId: string,
data: Partial<TrusteeOrganisation> data: Partial<TrusteeOrganisation>
): Promise<TrusteeOrganisation> { ): Promise<TrusteeOrganisation> {
return await request({ return await request({
url: `/api/trustee/organisations/${orgId}`, url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`,
method: 'put', method: 'put',
data data
}); });
@ -205,10 +225,11 @@ export async function updateOrganisation(
export async function deleteOrganisation( export async function deleteOrganisation(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
orgId: string orgId: string
): Promise<void> { ): Promise<void> {
await request({ await request({
url: `/api/trustee/organisations/${orgId}`, url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`,
method: 'delete' method: 'delete'
}); });
} }
@ -219,10 +240,11 @@ export async function deleteOrganisation(
export async function fetchRoles( export async function fetchRoles(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
params?: PaginationParams params?: PaginationParams
): Promise<PaginatedResponse<TrusteeRole> | TrusteeRole[]> { ): Promise<PaginatedResponse<TrusteeRole> | TrusteeRole[]> {
return await request({ return await request({
url: '/api/trustee/roles', url: `${_getTrusteeBaseUrl(instanceId)}/roles`,
method: 'get', method: 'get',
params: _buildPaginationParams(params) params: _buildPaginationParams(params)
}); });
@ -230,11 +252,12 @@ export async function fetchRoles(
export async function fetchRoleById( export async function fetchRoleById(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
roleId: string roleId: string
): Promise<TrusteeRole | null> { ): Promise<TrusteeRole | null> {
try { try {
return await request({ return await request({
url: `/api/trustee/roles/${roleId}`, url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`,
method: 'get' method: 'get'
}); });
} catch (error: any) { } catch (error: any) {
@ -245,10 +268,11 @@ export async function fetchRoleById(
export async function createRole( export async function createRole(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
data: Partial<TrusteeRole> data: Partial<TrusteeRole>
): Promise<TrusteeRole> { ): Promise<TrusteeRole> {
return await request({ return await request({
url: '/api/trustee/roles', url: `${_getTrusteeBaseUrl(instanceId)}/roles`,
method: 'post', method: 'post',
data data
}); });
@ -256,11 +280,12 @@ export async function createRole(
export async function updateRole( export async function updateRole(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
roleId: string, roleId: string,
data: Partial<TrusteeRole> data: Partial<TrusteeRole>
): Promise<TrusteeRole> { ): Promise<TrusteeRole> {
return await request({ return await request({
url: `/api/trustee/roles/${roleId}`, url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`,
method: 'put', method: 'put',
data data
}); });
@ -268,10 +293,11 @@ export async function updateRole(
export async function deleteRole( export async function deleteRole(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
roleId: string roleId: string
): Promise<void> { ): Promise<void> {
await request({ await request({
url: `/api/trustee/roles/${roleId}`, url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`,
method: 'delete' method: 'delete'
}); });
} }
@ -282,10 +308,11 @@ export async function deleteRole(
export async function fetchAccess( export async function fetchAccess(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
params?: PaginationParams params?: PaginationParams
): Promise<PaginatedResponse<TrusteeAccess> | TrusteeAccess[]> { ): Promise<PaginatedResponse<TrusteeAccess> | TrusteeAccess[]> {
return await request({ return await request({
url: '/api/trustee/access', url: `${_getTrusteeBaseUrl(instanceId)}/access`,
method: 'get', method: 'get',
params: _buildPaginationParams(params) params: _buildPaginationParams(params)
}); });
@ -293,11 +320,12 @@ export async function fetchAccess(
export async function fetchAccessById( export async function fetchAccessById(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
accessId: string accessId: string
): Promise<TrusteeAccess | null> { ): Promise<TrusteeAccess | null> {
try { try {
return await request({ return await request({
url: `/api/trustee/access/${accessId}`, url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`,
method: 'get' method: 'get'
}); });
} catch (error: any) { } catch (error: any) {
@ -308,30 +336,33 @@ export async function fetchAccessById(
export async function fetchAccessByOrganisation( export async function fetchAccessByOrganisation(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
orgId: string orgId: string
): Promise<TrusteeAccess[]> { ): Promise<TrusteeAccess[]> {
return await request({ return await request({
url: `/api/trustee/access/organisation/${orgId}`, url: `${_getTrusteeBaseUrl(instanceId)}/access/organisation/${orgId}`,
method: 'get' method: 'get'
}); });
} }
export async function fetchAccessByUser( export async function fetchAccessByUser(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
userId: string userId: string
): Promise<TrusteeAccess[]> { ): Promise<TrusteeAccess[]> {
return await request({ return await request({
url: `/api/trustee/access/user/${userId}`, url: `${_getTrusteeBaseUrl(instanceId)}/access/user/${userId}`,
method: 'get' method: 'get'
}); });
} }
export async function createAccess( export async function createAccess(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
data: Partial<TrusteeAccess> data: Partial<TrusteeAccess>
): Promise<TrusteeAccess> { ): Promise<TrusteeAccess> {
return await request({ return await request({
url: '/api/trustee/access', url: `${_getTrusteeBaseUrl(instanceId)}/access`,
method: 'post', method: 'post',
data data
}); });
@ -339,11 +370,12 @@ export async function createAccess(
export async function updateAccess( export async function updateAccess(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
accessId: string, accessId: string,
data: Partial<TrusteeAccess> data: Partial<TrusteeAccess>
): Promise<TrusteeAccess> { ): Promise<TrusteeAccess> {
return await request({ return await request({
url: `/api/trustee/access/${accessId}`, url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`,
method: 'put', method: 'put',
data data
}); });
@ -351,10 +383,11 @@ export async function updateAccess(
export async function deleteAccess( export async function deleteAccess(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
accessId: string accessId: string
): Promise<void> { ): Promise<void> {
await request({ await request({
url: `/api/trustee/access/${accessId}`, url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`,
method: 'delete' method: 'delete'
}); });
} }
@ -365,10 +398,11 @@ export async function deleteAccess(
export async function fetchContracts( export async function fetchContracts(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
params?: PaginationParams params?: PaginationParams
): Promise<PaginatedResponse<TrusteeContract> | TrusteeContract[]> { ): Promise<PaginatedResponse<TrusteeContract> | TrusteeContract[]> {
return await request({ return await request({
url: '/api/trustee/contracts', url: `${_getTrusteeBaseUrl(instanceId)}/contracts`,
method: 'get', method: 'get',
params: _buildPaginationParams(params) params: _buildPaginationParams(params)
}); });
@ -376,11 +410,12 @@ export async function fetchContracts(
export async function fetchContractById( export async function fetchContractById(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
contractId: string contractId: string
): Promise<TrusteeContract | null> { ): Promise<TrusteeContract | null> {
try { try {
return await request({ return await request({
url: `/api/trustee/contracts/${contractId}`, url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`,
method: 'get' method: 'get'
}); });
} catch (error: any) { } catch (error: any) {
@ -391,20 +426,22 @@ export async function fetchContractById(
export async function fetchContractsByOrganisation( export async function fetchContractsByOrganisation(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
orgId: string orgId: string
): Promise<TrusteeContract[]> { ): Promise<TrusteeContract[]> {
return await request({ return await request({
url: `/api/trustee/contracts/organisation/${orgId}`, url: `${_getTrusteeBaseUrl(instanceId)}/contracts/organisation/${orgId}`,
method: 'get' method: 'get'
}); });
} }
export async function createContract( export async function createContract(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
data: Partial<TrusteeContract> data: Partial<TrusteeContract>
): Promise<TrusteeContract> { ): Promise<TrusteeContract> {
return await request({ return await request({
url: '/api/trustee/contracts', url: `${_getTrusteeBaseUrl(instanceId)}/contracts`,
method: 'post', method: 'post',
data data
}); });
@ -412,11 +449,12 @@ export async function createContract(
export async function updateContract( export async function updateContract(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
contractId: string, contractId: string,
data: Partial<TrusteeContract> data: Partial<TrusteeContract>
): Promise<TrusteeContract> { ): Promise<TrusteeContract> {
return await request({ return await request({
url: `/api/trustee/contracts/${contractId}`, url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`,
method: 'put', method: 'put',
data data
}); });
@ -424,10 +462,11 @@ export async function updateContract(
export async function deleteContract( export async function deleteContract(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
contractId: string contractId: string
): Promise<void> { ): Promise<void> {
await request({ await request({
url: `/api/trustee/contracts/${contractId}`, url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`,
method: 'delete' method: 'delete'
}); });
} }
@ -438,10 +477,11 @@ export async function deleteContract(
export async function fetchDocuments( export async function fetchDocuments(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
params?: PaginationParams params?: PaginationParams
): Promise<PaginatedResponse<TrusteeDocument> | TrusteeDocument[]> { ): Promise<PaginatedResponse<TrusteeDocument> | TrusteeDocument[]> {
return await request({ return await request({
url: '/api/trustee/documents', url: `${_getTrusteeBaseUrl(instanceId)}/documents`,
method: 'get', method: 'get',
params: _buildPaginationParams(params) params: _buildPaginationParams(params)
}); });
@ -449,11 +489,12 @@ export async function fetchDocuments(
export async function fetchDocumentById( export async function fetchDocumentById(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
documentId: string documentId: string
): Promise<TrusteeDocument | null> { ): Promise<TrusteeDocument | null> {
try { try {
return await request({ return await request({
url: `/api/trustee/documents/${documentId}`, url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`,
method: 'get' method: 'get'
}); });
} catch (error: any) { } catch (error: any) {
@ -464,20 +505,22 @@ export async function fetchDocumentById(
export async function fetchDocumentsByContract( export async function fetchDocumentsByContract(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
contractId: string contractId: string
): Promise<TrusteeDocument[]> { ): Promise<TrusteeDocument[]> {
return await request({ return await request({
url: `/api/trustee/documents/contract/${contractId}`, url: `${_getTrusteeBaseUrl(instanceId)}/documents/contract/${contractId}`,
method: 'get' method: 'get'
}); });
} }
export async function createDocument( export async function createDocument(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
data: Partial<TrusteeDocument> data: Partial<TrusteeDocument>
): Promise<TrusteeDocument> { ): Promise<TrusteeDocument> {
return await request({ return await request({
url: '/api/trustee/documents', url: `${_getTrusteeBaseUrl(instanceId)}/documents`,
method: 'post', method: 'post',
data data
}); });
@ -485,11 +528,12 @@ export async function createDocument(
export async function updateDocument( export async function updateDocument(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
documentId: string, documentId: string,
data: Partial<TrusteeDocument> data: Partial<TrusteeDocument>
): Promise<TrusteeDocument> { ): Promise<TrusteeDocument> {
return await request({ return await request({
url: `/api/trustee/documents/${documentId}`, url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`,
method: 'put', method: 'put',
data data
}); });
@ -497,10 +541,11 @@ export async function updateDocument(
export async function deleteDocument( export async function deleteDocument(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
documentId: string documentId: string
): Promise<void> { ): Promise<void> {
await request({ await request({
url: `/api/trustee/documents/${documentId}`, url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`,
method: 'delete' method: 'delete'
}); });
} }
@ -511,10 +556,11 @@ export async function deleteDocument(
export async function fetchPositions( export async function fetchPositions(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
params?: PaginationParams params?: PaginationParams
): Promise<PaginatedResponse<TrusteePosition> | TrusteePosition[]> { ): Promise<PaginatedResponse<TrusteePosition> | TrusteePosition[]> {
return await request({ return await request({
url: '/api/trustee/positions', url: `${_getTrusteeBaseUrl(instanceId)}/positions`,
method: 'get', method: 'get',
params: _buildPaginationParams(params) params: _buildPaginationParams(params)
}); });
@ -522,11 +568,12 @@ export async function fetchPositions(
export async function fetchPositionById( export async function fetchPositionById(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
positionId: string positionId: string
): Promise<TrusteePosition | null> { ): Promise<TrusteePosition | null> {
try { try {
return await request({ return await request({
url: `/api/trustee/positions/${positionId}`, url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`,
method: 'get' method: 'get'
}); });
} catch (error: any) { } catch (error: any) {
@ -537,30 +584,33 @@ export async function fetchPositionById(
export async function fetchPositionsByContract( export async function fetchPositionsByContract(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
contractId: string contractId: string
): Promise<TrusteePosition[]> { ): Promise<TrusteePosition[]> {
return await request({ return await request({
url: `/api/trustee/positions/contract/${contractId}`, url: `${_getTrusteeBaseUrl(instanceId)}/positions/contract/${contractId}`,
method: 'get' method: 'get'
}); });
} }
export async function fetchPositionsByOrganisation( export async function fetchPositionsByOrganisation(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
orgId: string orgId: string
): Promise<TrusteePosition[]> { ): Promise<TrusteePosition[]> {
return await request({ return await request({
url: `/api/trustee/positions/organisation/${orgId}`, url: `${_getTrusteeBaseUrl(instanceId)}/positions/organisation/${orgId}`,
method: 'get' method: 'get'
}); });
} }
export async function createPosition( export async function createPosition(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
data: Partial<TrusteePosition> data: Partial<TrusteePosition>
): Promise<TrusteePosition> { ): Promise<TrusteePosition> {
return await request({ return await request({
url: '/api/trustee/positions', url: `${_getTrusteeBaseUrl(instanceId)}/positions`,
method: 'post', method: 'post',
data data
}); });
@ -568,11 +618,12 @@ export async function createPosition(
export async function updatePosition( export async function updatePosition(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
positionId: string, positionId: string,
data: Partial<TrusteePosition> data: Partial<TrusteePosition>
): Promise<TrusteePosition> { ): Promise<TrusteePosition> {
return await request({ return await request({
url: `/api/trustee/positions/${positionId}`, url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`,
method: 'put', method: 'put',
data data
}); });
@ -580,10 +631,11 @@ export async function updatePosition(
export async function deletePosition( export async function deletePosition(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
positionId: string positionId: string
): Promise<void> { ): Promise<void> {
await request({ await request({
url: `/api/trustee/positions/${positionId}`, url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`,
method: 'delete' method: 'delete'
}); });
} }
@ -594,10 +646,11 @@ export async function deletePosition(
export async function fetchPositionDocuments( export async function fetchPositionDocuments(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
params?: PaginationParams params?: PaginationParams
): Promise<PaginatedResponse<TrusteePositionDocument> | TrusteePositionDocument[]> { ): Promise<PaginatedResponse<TrusteePositionDocument> | TrusteePositionDocument[]> {
return await request({ return await request({
url: '/api/trustee/position-documents', url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`,
method: 'get', method: 'get',
params: _buildPaginationParams(params) params: _buildPaginationParams(params)
}); });
@ -605,11 +658,12 @@ export async function fetchPositionDocuments(
export async function fetchPositionDocumentById( export async function fetchPositionDocumentById(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
linkId: string linkId: string
): Promise<TrusteePositionDocument | null> { ): Promise<TrusteePositionDocument | null> {
try { try {
return await request({ return await request({
url: `/api/trustee/position-documents/${linkId}`, url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`,
method: 'get' method: 'get'
}); });
} catch (error: any) { } catch (error: any) {
@ -620,30 +674,33 @@ export async function fetchPositionDocumentById(
export async function fetchDocumentsForPosition( export async function fetchDocumentsForPosition(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
positionId: string positionId: string
): Promise<TrusteePositionDocument[]> { ): Promise<TrusteePositionDocument[]> {
return await request({ return await request({
url: `/api/trustee/position-documents/position/${positionId}`, url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/position/${positionId}`,
method: 'get' method: 'get'
}); });
} }
export async function fetchPositionsForDocument( export async function fetchPositionsForDocument(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
documentId: string documentId: string
): Promise<TrusteePositionDocument[]> { ): Promise<TrusteePositionDocument[]> {
return await request({ return await request({
url: `/api/trustee/position-documents/document/${documentId}`, url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/document/${documentId}`,
method: 'get' method: 'get'
}); });
} }
export async function createPositionDocument( export async function createPositionDocument(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
data: Partial<TrusteePositionDocument> data: Partial<TrusteePositionDocument>
): Promise<TrusteePositionDocument> { ): Promise<TrusteePositionDocument> {
return await request({ return await request({
url: '/api/trustee/position-documents', url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`,
method: 'post', method: 'post',
data data
}); });
@ -651,10 +708,11 @@ export async function createPositionDocument(
export async function deletePositionDocument( export async function deletePositionDocument(
request: ApiRequestFunction, request: ApiRequestFunction,
instanceId: string,
linkId: string linkId: string
): Promise<void> { ): Promise<void> {
await request({ await request({
url: `/api/trustee/position-documents/${linkId}`, url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`,
method: 'delete' method: 'delete'
}); });
} }

View file

@ -13,8 +13,10 @@ export interface User {
enabled: boolean; enabled: boolean;
roleLabels?: string[]; // Array of role labels from backend (e.g., ["user"]) roleLabels?: string[]; // Array of role labels from backend (e.g., ["user"])
authenticationAuthority: string; authenticationAuthority: string;
mandateId: string; isSysAdmin?: boolean; // System-Administrator Flag
[key: string]: any; // Allow additional properties (may include deprecated 'privilege' from backend) // 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<Omit<User, 'id' | 'mandateId'>>; export type UserUpdateData = Partial<Omit<User, 'id' | 'mandateId'>>;

View file

@ -84,15 +84,23 @@ export function EditActionButton<T = any>({
const handleClick = async (e: React.MouseEvent) => { const handleClick = async (e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
if (!isDisabled && !loading && !isEditing && !internalLoading && !fetchingData && !isPopupOpen) { 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); setInternalLoading(true);
setFetchingData(true); setFetchingData(true);
try { try {
// Call the onEdit callback if provided
if (onEdit) {
await onEdit(row);
}
const itemId = (row as any)[idField]; const itemId = (row as any)[idField];
// Fetch current item data - use generic fetch function from hookData // Fetch current item data - use generic fetch function from hookData

View file

@ -2,6 +2,7 @@
* MandateNavigation * MandateNavigation
* *
* Hierarchische Navigation für das Multi-Tenant-System. * Hierarchische Navigation für das Multi-Tenant-System.
* Verwendet TreeNavigation für flexible Baumstruktur.
* *
* Struktur: * Struktur:
* - SYSTEM (immer verfügbar) * - SYSTEM (immer verfügbar)
@ -16,13 +17,13 @@
* - ADMINISTRATION (nur für SysAdmin) * - ADMINISTRATION (nur für SysAdmin)
*/ */
import React, { useState } from 'react'; import React, { useMemo } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import { useMandates, useFeatureStore } from '../../stores/featureStore'; import { useMandates, useFeatureStore } from '../../stores/featureStore';
import { useCurrentUser } from '../../hooks/useUsers';
import { FEATURE_REGISTRY, getLabel } from '../../types/mandate'; import { FEATURE_REGISTRY, getLabel } from '../../types/mandate';
import type { Mandate, MandateFeature, FeatureInstance } from '../../types/mandate'; import type { Mandate, MandateFeature, FeatureInstance } from '../../types/mandate';
import { FaHome, FaCog, FaChevronDown, FaChevronRight, FaBriefcase, FaRobot, FaPlay } from 'react-icons/fa'; import { FaHome, FaCog, FaBriefcase, FaRobot, FaPlay, FaBuilding, FaUsers, FaUserShield } from 'react-icons/fa';
import { RiAdminFill } from 'react-icons/ri'; import { TreeNavigation, type TreeItem, type TreeNodeItem } from './TreeNavigation';
import styles from './MandateNavigation.module.css'; import styles from './MandateNavigation.module.css';
// ============================================================================= // =============================================================================
@ -36,263 +37,93 @@ const FEATURE_ICONS: Record<string, React.ReactNode> = {
}; };
// ============================================================================= // =============================================================================
// SYSTEM SECTION // HELPER FUNCTIONS
// ============================================================================= // =============================================================================
const SystemSection: React.FC = () => { /**
const location = useLocation(); * Convert a FeatureInstance to TreeNodeItem
*/
return ( function instanceToTreeNode(
<div className={styles.section}> instance: FeatureInstance,
<div className={styles.sectionHeader}> mandateId: string,
<span className={styles.sectionTitle}>SYSTEM</span> featureCode: string
</div> ): TreeNodeItem {
<div className={styles.sectionContent}>
<NavLink
to="/"
className={({ isActive }) =>
`${styles.navItem} ${isActive && location.pathname === '/' ? styles.active : ''}`
}
>
<FaHome className={styles.navIcon} />
<span>Übersicht</span>
</NavLink>
<NavLink
to="/settings"
className={({ isActive }) =>
`${styles.navItem} ${isActive ? styles.active : ''}`
}
>
<FaCog className={styles.navIcon} />
<span>Einstellungen</span>
</NavLink>
</div>
</div>
);
};
// =============================================================================
// INSTANCE NAV GROUP
// =============================================================================
interface InstanceNavGroupProps {
instance: FeatureInstance;
mandateId: string;
featureCode: string;
}
const InstanceNavGroup: React.FC<InstanceNavGroupProps> = ({
instance,
mandateId,
featureCode,
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const location = useLocation();
// Prüfe ob wir in dieser Instanz sind
const basePath = `/mandates/${mandateId}/${featureCode}/${instance.id}`; const basePath = `/mandates/${mandateId}/${featureCode}/${instance.id}`;
const isInInstance = location.pathname.startsWith(basePath);
// Auto-expand wenn wir in der Instanz sind // Get views from registry
React.useEffect(() => {
if (isInInstance && !isExpanded) {
setIsExpanded(true);
}
}, [isInInstance]);
// Views aus Registry holen
const featureConfig = FEATURE_REGISTRY[featureCode]; const featureConfig = FEATURE_REGISTRY[featureCode];
const views = featureConfig?.views || []; 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 visibleViews = views.filter(view => {
const viewCode = `${featureCode}-${view.code}`; const viewCode = `${featureCode}-${view.code}`;
return instance.permissions?.views?.[viewCode] !== false; return instance.permissions?.views?.[viewCode] !== false;
}); });
return ( // Convert views to children
<div className={`${styles.instanceGroup} ${isInInstance ? styles.activeInstance : ''}`}> const children: TreeNodeItem[] = visibleViews.map(view => ({
<button id: `${instance.id}-${view.code}`,
className={styles.instanceHeader} label: getLabel(view.label),
onClick={() => setIsExpanded(!isExpanded)} path: `${basePath}/${view.path}`,
> }));
{isExpanded ? <FaChevronDown className={styles.chevron} /> : <FaChevronRight className={styles.chevron} />}
<span className={styles.instanceLabel}>{instance.instanceLabel}</span>
<span className={styles.roleBadge}>{instance.userRole}</span>
</button>
{isExpanded && ( return {
<div className={styles.instanceViews}> id: instance.id,
{visibleViews.map(view => ( label: instance.instanceLabel,
<NavLink badge: instance.userRole,
key={view.code} children,
to={`${basePath}/${view.path}`} defaultExpanded: false,
className={({ isActive }) =>
`${styles.viewItem} ${isActive ? styles.active : ''}`
}
>
<span>{getLabel(view.label)}</span>
</NavLink>
))}
</div>
)}
</div>
);
}; };
// =============================================================================
// FEATURE NAV GROUP
// =============================================================================
interface FeatureNavGroupProps {
feature: MandateFeature;
mandateId: string;
} }
const FeatureNavGroup: React.FC<FeatureNavGroupProps> = ({ feature, mandateId }) => { /**
const [isExpanded, setIsExpanded] = useState(false); * Convert a MandateFeature to TreeNodeItem
const location = useLocation(); */
function featureToTreeNode(
// Prüfe ob wir in diesem Feature sind feature: MandateFeature,
const featurePath = `/mandates/${mandateId}/${feature.code}`; mandateId: string
const isInFeature = location.pathname.startsWith(featurePath); ): TreeNodeItem | null {
// Auto-expand wenn wir im Feature sind
React.useEffect(() => {
if (isInFeature && !isExpanded) {
setIsExpanded(true);
}
}, [isInFeature]);
if (feature.instances.length === 0) { if (feature.instances.length === 0) {
return null; return null;
} }
return ( const children = feature.instances.map(instance =>
<div className={`${styles.featureGroup} ${isInFeature ? styles.activeFeature : ''}`}> instanceToTreeNode(instance, mandateId, feature.code)
<button
className={styles.featureHeader}
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? <FaChevronDown className={styles.chevron} /> : <FaChevronRight className={styles.chevron} />}
<span className={styles.featureIcon}>
{FEATURE_ICONS[feature.code] || <FaBriefcase />}
</span>
<span className={styles.featureLabel}>{getLabel(feature.label)}</span>
<span className={styles.instanceCount}>{feature.instances.length}</span>
</button>
{isExpanded && (
<div className={styles.featureContent}>
{feature.instances.map(instance => (
<InstanceNavGroup
key={instance.id}
instance={instance}
mandateId={mandateId}
featureCode={feature.code}
/>
))}
</div>
)}
</div>
); );
return {
id: `${mandateId}-${feature.code}`,
label: getLabel(feature.label),
icon: FEATURE_ICONS[feature.code] || <FaBriefcase />,
badge: feature.instances.length,
children,
defaultExpanded: false,
}; };
// =============================================================================
// MANDATE NAV GROUP
// =============================================================================
interface MandateNavGroupProps {
mandate: Mandate;
} }
const MandateNavGroup: React.FC<MandateNavGroupProps> = ({ mandate }) => { /**
const [isExpanded, setIsExpanded] = useState(true); * Convert a Mandate to TreeNodeItem
const location = useLocation(); */
function mandateToTreeNode(mandate: Mandate): TreeNodeItem | null {
// Prüfe ob wir in diesem Mandanten sind
const mandatePath = `/mandates/${mandate.id}`;
const isInMandate = location.pathname.startsWith(mandatePath);
if (mandate.features.length === 0) { if (mandate.features.length === 0) {
return null; return null;
} }
return ( const children = mandate.features
<div className={`${styles.mandateGroup} ${isInMandate ? styles.activeMandate : ''}`}> .map(feature => featureToTreeNode(feature, mandate.id))
<button .filter((node): node is TreeNodeItem => node !== null);
className={styles.mandateHeader}
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? <FaChevronDown className={styles.chevron} /> : <FaChevronRight className={styles.chevron} />}
<span className={styles.mandateLabel}>{mandate.name}</span>
</button>
{isExpanded && ( if (children.length === 0) {
<div className={styles.mandateContent}>
{mandate.features.map(feature => (
<FeatureNavGroup
key={feature.code}
feature={feature}
mandateId={mandate.id}
/>
))}
</div>
)}
</div>
);
};
// =============================================================================
// ADMIN SECTION
// =============================================================================
interface AdminSectionProps {
isSysAdmin: boolean;
}
const AdminSection: React.FC<AdminSectionProps> = ({ isSysAdmin }) => {
if (!isSysAdmin) {
return null; return null;
} }
return ( return {
<div className={styles.section}> id: mandate.id,
<div className={styles.sectionHeader}> label: mandate.name,
<span className={styles.sectionTitle}>ADMINISTRATION</span> children,
</div> defaultExpanded: true,
<div className={styles.sectionContent}>
<NavLink
to="/admin/mandates"
className={({ isActive }) =>
`${styles.navItem} ${isActive ? styles.active : ''}`
}
>
<RiAdminFill className={styles.navIcon} />
<span>Mandanten</span>
</NavLink>
<NavLink
to="/admin/users"
className={({ isActive }) =>
`${styles.navItem} ${isActive ? styles.active : ''}`
}
>
<RiAdminFill className={styles.navIcon} />
<span>Benutzer</span>
</NavLink>
<NavLink
to="/admin/roles"
className={({ isActive }) =>
`${styles.navItem} ${isActive ? styles.active : ''}`
}
>
<RiAdminFill className={styles.navIcon} />
<span>Globale Rollen</span>
</NavLink>
</div>
</div>
);
}; };
}
// ============================================================================= // =============================================================================
// EMPTY STATE // EMPTY STATE
@ -314,32 +145,97 @@ const EmptyState: React.FC = () => (
export const MandateNavigation: React.FC = () => { export const MandateNavigation: React.FC = () => {
const mandates = useMandates(); const mandates = useMandates();
const { hasAnyInstance } = useFeatureStore(); const { hasAnyInstance } = useFeatureStore();
const { user } = useCurrentUser();
// TODO: Aus Auth-Store holen // Get isSysAdmin from user data
const isSysAdmin = false; 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: <FaHome />,
path: '/',
},
{
id: 'settings',
label: 'Einstellungen',
icon: <FaCog />,
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: <FaBuilding />,
path: '/admin/mandates',
},
{
id: 'admin-users',
label: 'Benutzer',
icon: <FaUsers />,
path: '/admin/users',
},
{
id: 'admin-roles',
label: 'Globale Rollen',
icon: <FaUserShield />,
path: '/admin/roles',
},
],
});
}
return items;
}, [mandates, hasAnyInstance, isSysAdmin]);
return ( return (
<div className={styles.navigation}> <div className={styles.navigation}>
{/* System-Bereich (immer sichtbar) */} {hasAnyInstance() || isSysAdmin ? (
<SystemSection /> <TreeNavigation
items={navigationItems}
{/* Separator */} autoExpandActive={true}
<div className={styles.separator} /> />
{/* Mandanten & Features */}
{hasAnyInstance() ? (
mandates.map(mandate => (
<MandateNavGroup key={mandate.id} mandate={mandate} />
))
) : ( ) : (
<>
<TreeNavigation
items={navigationItems.slice(0, 2)} // System section + separator
autoExpandActive={true}
/>
<EmptyState /> <EmptyState />
</>
)} )}
{/* Separator vor Admin */}
{isSysAdmin && <div className={styles.separator} />}
{/* Admin-Bereich (nur für SysAdmin) */}
<AdminSection isSysAdmin={isSysAdmin} />
</div> </div>
); );
}; };

View file

@ -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;
}

View file

@ -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<TreeNodeProps> = ({
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 && (
<span className={styles.chevron} onClick={handleChevronClick}>
{isExpanded ? <FaChevronDown /> : <FaChevronRight />}
</span>
)}
{!isExpandable && hasChildren === false && (
<span className={styles.chevronSpacer} />
)}
{node.icon && <span className={styles.nodeIcon}>{node.icon}</span>}
<span className={styles.nodeLabel}>{node.label}</span>
{node.badge !== undefined && (
<span
className={`${styles.nodeBadge} ${node.badgeVariant ? styles[`badge${node.badgeVariant.charAt(0).toUpperCase() + node.badgeVariant.slice(1)}`] : ''}`}
>
{node.badge}
</span>
)}
</>
);
// 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 ? (
<NavLink
to={node.path}
className={nodeClasses}
onClick={handleClick}
data-id={node.dataId}
>
{nodeContent}
</NavLink>
) : (
<button
type="button"
className={nodeClasses}
onClick={handleClick}
disabled={node.disabled}
data-id={node.dataId}
>
{nodeContent}
</button>
);
// Check max depth
const canRenderChildren = maxDepth === 0 || level < maxDepth;
return (
<div className={styles.treeNodeContainer}>
{nodeElement}
{isExpanded && hasChildren && canRenderChildren && (
<div className={styles.treeNodeChildren}>
{node.children!.map((child, index) => (
<TreeNode
key={child.id || `${node.id}-child-${index}`}
node={child}
level={level + 1}
autoExpandActive={autoExpandActive}
currentPath={currentPath}
onNodeClick={onNodeClick}
maxDepth={maxDepth}
/>
))}
</div>
)}
</div>
);
};
// =============================================================================
// TREE SECTION COMPONENT
// =============================================================================
interface TreeSectionProps {
section: TreeSectionItem;
autoExpandActive: boolean;
currentPath: string;
onNodeClick?: (node: TreeNodeItem) => void;
maxDepth: number;
}
const TreeSection: React.FC<TreeSectionProps> = ({
section,
autoExpandActive,
currentPath,
onNodeClick,
maxDepth,
}) => {
if (section.visible === false) {
return null;
}
return (
<div className={styles.treeSection}>
<div className={styles.sectionHeader}>
<span className={styles.sectionTitle}>{section.title}</span>
</div>
<div className={styles.sectionContent}>
{section.children.map((node, index) => (
<TreeNode
key={node.id || `section-${section.title}-${index}`}
node={node}
level={0}
autoExpandActive={autoExpandActive}
currentPath={currentPath}
onNodeClick={onNodeClick}
maxDepth={maxDepth}
/>
))}
</div>
</div>
);
};
// =============================================================================
// MAIN COMPONENT
// =============================================================================
export const TreeNavigation: React.FC<TreeNavigationProps> = ({
items,
autoExpandActive = true,
onNodeClick,
maxDepth = 0,
className = '',
}) => {
const location = useLocation();
const currentPath = location.pathname;
return (
<nav className={`${styles.treeNavigation} ${className}`}>
{items.map((item, index) => {
if (isTreeSeparator(item)) {
return <div key={`separator-${index}`} className={styles.separator} />;
}
if (isTreeSection(item)) {
return (
<TreeSection
key={`section-${item.title}-${index}`}
section={item}
autoExpandActive={autoExpandActive}
currentPath={currentPath}
onNodeClick={onNodeClick}
maxDepth={maxDepth}
/>
);
}
if (isTreeNode(item)) {
return (
<TreeNode
key={item.id || `node-${index}`}
node={item}
level={0}
autoExpandActive={autoExpandActive}
currentPath={currentPath}
onNodeClick={onNodeClick}
maxDepth={maxDepth}
/>
);
}
return null;
})}
</nav>
);
};
export default TreeNavigation;

View file

@ -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';

View file

@ -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);
}

View file

@ -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 (
<div className={styles.userSection}>
<button
className={styles.userButton}
onClick={() => setShowMenu(!showMenu)}
aria-expanded={showMenu}
>
<div className={styles.avatar}>
{initials}
</div>
<div className={styles.userInfo}>
<span className={styles.userName}>{user.fullName || user.username}</span>
<span className={styles.userEmail}>{user.email}</span>
</div>
<span className={styles.chevron}>
{showMenu ? '▲' : '▼'}
</span>
</button>
{showMenu && (
<div className={styles.menu}>
<button
className={styles.menuItem}
onClick={handleSettings}
>
<span className={styles.menuIcon}></span>
Einstellungen
</button>
<button
className={styles.menuItem}
onClick={handleLegal}
>
<span className={styles.menuIcon}>📜</span>
Rechtliche Hinweise
</button>
<div className={styles.menuDivider} />
<button
className={styles.menuItem}
onClick={handleLogout}
disabled={isLoggingOut}
>
<span className={styles.menuIcon}>🚪</span>
{isLoggingOut ? 'Abmelden...' : 'Abmelden'}
</button>
</div>
)}
{/* Legal Modal */}
{showLegalModal && (
<div className={styles.modalOverlay} onClick={() => setShowLegalModal(false)}>
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2>Rechtliche Hinweise</h2>
<button
className={styles.modalClose}
onClick={() => setShowLegalModal(false)}
>
</button>
</div>
<div className={styles.modalContent}>
<div className={styles.legalSection}>
<h3>Datenverarbeitung und KI-Nutzung</h3>
<h4>1. Einwilligung zur Datenverarbeitung</h4>
<p>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:</p>
<ul>
<li>Sie autorisieren die Erfassung, Verarbeitung, Übertragung und Speicherung aller Daten, die Sie bei der Nutzung unserer Dienste bereitstellen.</li>
<li>Nutzerdaten können an Drittanbieter von künstlicher Intelligenz übertragen werden (z.B. OpenAI).</li>
<li>Diese Einwilligung erstreckt sich auf alle Inhalte, einschließlich Text, Bilder, Dokumente und Gesprächsverläufe.</li>
</ul>
<h4>2. Anerkennung der KI-Verarbeitungsrisiken</h4>
<ul>
<li>KI-Systeme können unerwartete oder ungenaue Ausgaben erzeugen.</li>
<li>KI-Dienste können Daten gemäß ihren eigenen Nutzungsbedingungen speichern oder daraus lernen.</li>
<li>Trotz Sicherheitsmaßnahmen können Daten anfällig für unbefugten Zugriff sein.</li>
</ul>
<h4>3. Haftungsausschluss</h4>
<p>Im größtmöglichen Umfang verzichten Sie auf Ansprüche, die sich aus der KI-Verarbeitung ergeben, einschließlich Datenverletzungen und unbeabsichtigter Offenlegung.</p>
</div>
<div className={styles.legalLinks}>
<a href="/poweron-privacy.html" target="_blank" rel="noopener noreferrer">
Datenschutzrichtlinie
</a>
<a href="/poweron-terms.html" target="_blank" rel="noopener noreferrer">
Nutzungsbedingungen
</a>
<a href="/poweron-home.html" target="_blank" rel="noopener noreferrer">
Über PowerOn
</a>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default UserSection;

View file

@ -70,7 +70,7 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ isMinimized = false }) => {
enabled: cached.enabled ?? true, // Assume enabled if logged in enabled: cached.enabled ?? true, // Assume enabled if logged in
roleLabels: cached.roleLabels || [], roleLabels: cached.roleLabels || [],
authenticationAuthority: cached.authenticationAuthority || 'local', authenticationAuthority: cached.authenticationAuthority || 'local',
mandateId: cached.mandateId || '' isSysAdmin: cached.isSysAdmin || false
}; };
setUser(userData); setUser(userData);
setUserError(null); setUserError(null);
@ -99,7 +99,7 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ isMinimized = false }) => {
enabled: cached.enabled ?? true, enabled: cached.enabled ?? true,
roleLabels: cached.roleLabels || [], roleLabels: cached.roleLabels || [],
authenticationAuthority: cached.authenticationAuthority || 'local', authenticationAuthority: cached.authenticationAuthority || 'local',
mandateId: cached.mandateId || '' isSysAdmin: cached.isSysAdmin || false
}; };
setUser(userData); setUser(userData);
setUserError(null); setUserError(null);

View file

@ -15,12 +15,8 @@ import type { FeatureInstance, Mandate, MandateFeature } from '../types/mandate'
// URL PARAMETER TYPES // URL PARAMETER TYPES
// ============================================================================= // =============================================================================
export interface FeatureRouteParams { // Route-Parameter werden als Record<string, string | undefined> erwartet
mandateId?: string; // Wir verwenden daher einen einfachen Typ-Alias
featureCode?: string;
instanceId?: string;
'*'?: string; // Wildcard für Sub-Pfade
}
// ============================================================================= // =============================================================================
// RETURN TYPES // RETURN TYPES
@ -63,7 +59,7 @@ export interface CurrentInstanceContext {
* ``` * ```
*/ */
export function useCurrentInstance(): CurrentInstanceContext { export function useCurrentInstance(): CurrentInstanceContext {
const params = useParams<FeatureRouteParams>(); const params = useParams();
const { getMandateById, getFeatureByCode, getInstanceById, loading } = useFeatureStore(); const { getMandateById, getFeatureByCode, getInstanceById, loading } = useFeatureStore();
const mandateId = params.mandateId; const mandateId = params.mandateId;
@ -108,7 +104,7 @@ export function useInstance(): FeatureInstance | undefined {
* Hook für die Instanz-ID aus der URL * Hook für die Instanz-ID aus der URL
*/ */
export function useInstanceId(): string | undefined { export function useInstanceId(): string | undefined {
const params = useParams<FeatureRouteParams>(); const params = useParams();
return params.instanceId; return params.instanceId;
} }
@ -116,7 +112,7 @@ export function useInstanceId(): string | undefined {
* Hook für den Feature-Code aus der URL * Hook für den Feature-Code aus der URL
*/ */
export function useFeatureCode(): string | undefined { export function useFeatureCode(): string | undefined {
const params = useParams<FeatureRouteParams>(); const params = useParams();
return params.featureCode; return params.featureCode;
} }
@ -124,7 +120,7 @@ export function useFeatureCode(): string | undefined {
* Hook für die Mandate-ID aus der URL * Hook für die Mandate-ID aus der URL
*/ */
export function useMandateId(): string | undefined { export function useMandateId(): string | undefined {
const params = useParams<FeatureRouteParams>(); const params = useParams();
return params.mandateId; return params.mandateId;
} }

View file

@ -5,7 +5,7 @@
* Die Berechtigungen werden summarisch pro Instanz geladen (kein einzelner API-Call pro Check). * 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 { useCurrentInstance } from './useCurrentInstance';
import type { import type {
TablePermission, TablePermission,
@ -27,10 +27,6 @@ const NO_ACCESS_TABLE: TablePermission = {
delete: 'n', delete: 'n',
}; };
const NO_ACCESS_FIELD: FieldPermission = {
read: false,
write: false,
};
// ============================================================================= // =============================================================================
// TABLE PERMISSION HOOKS // TABLE PERMISSION HOOKS

235
src/hooks/useMandates.ts Normal file
View file

@ -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<Mandate[]>([]);
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
const [pagination, setPagination] = useState<{
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
} | null>(null);
const { request, isLoading: loading, error } = useApiRequest<null, Mandate[]>();
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<Mandate>) => {
setMandates(prev =>
prev.map(m => m.id === mandateId ? { ...m, ...updateData } : m)
);
};
// Fetch single mandate
const fetchMandateById = useCallback(async (mandateId: string): Promise<Mandate | null> => {
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<Mandate>): Promise<boolean> => {
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<boolean> => {
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<boolean> => {
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<Mandate>
): Promise<void> => {
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;

View file

@ -1,6 +1,5 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useApiRequest } from './useApi'; import { useApiRequest } from './useApi';
import { getUserDataCache } from '../utils/userCache';
import api from '../api'; import api from '../api';
import { usePermissions, type UserPermissions } from './usePermissions'; import { usePermissions, type UserPermissions } from './usePermissions';
import { import {
@ -507,13 +506,9 @@ export function usePromptOperations() {
setCreatingPrompt(true); setCreatingPrompt(true);
try { try {
// Get mandateId from currentUser in sessionStorage cache // mandateId wird nicht mehr vom Client gesendet
const currentUserData = getUserDataCache(); // Das Backend bestimmt den Kontext über die instanceId im Request
const mandateId = currentUserData?.mandateId || '';
// Structure the request body as required by the API
const requestBody = { const requestBody = {
mandateId: mandateId,
name: promptData.name, name: promptData.name,
content: promptData.content content: promptData.content
}; };
@ -533,13 +528,8 @@ export function usePromptOperations() {
setUpdateError(null); setUpdateError(null);
try { try {
// Get mandateId from currentUser in sessionStorage cache // mandateId wird nicht mehr vom Client gesendet
const currentUserData = getUserDataCache();
const mandateId = currentUserData?.mandateId || '';
// Structure the request body as required by the API
const requestBody = { const requestBody = {
mandateId: mandateId,
name: updateData.name, name: updateData.name,
content: updateData.content content: updateData.content
}; };

235
src/hooks/useRoles.ts Normal file
View file

@ -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<Role[]>([]);
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
const [pagination, setPagination] = useState<{
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
} | null>(null);
const { request, isLoading: loading, error } = useApiRequest<null, Role[]>();
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<Role>) => {
setRoles(prev =>
prev.map(r => r.id === roleId ? { ...r, ...updateData } : r)
);
};
// Fetch single role
const fetchRoleById = useCallback(async (roleId: string): Promise<Role | null> => {
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<Role>): Promise<boolean> => {
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<boolean> => {
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<boolean> => {
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<Role>
): Promise<void> => {
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;

View file

@ -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 { useState, useEffect, useCallback } from 'react';
import { useApiRequest } from './useApi'; import { useApiRequest } from './useApi';
import api from '../api'; import api from '../api';
import { usePermissions, type UserPermissions } from './usePermissions'; import { usePermissions, type UserPermissions } from './usePermissions';
import { useInstanceId } from './useCurrentInstance';
import { import {
// Types // Types
type TrusteeOrganisation, type TrusteeOrganisation,
@ -94,15 +102,18 @@ export interface AttributeDefinition {
interface TrusteeEntityConfig<T> { interface TrusteeEntityConfig<T> {
entityName: string; entityName: string;
fetchAll: (request: any, params?: PaginationParams) => Promise<any>; fetchAll: (request: any, instanceId: string, params?: PaginationParams) => Promise<any>;
fetchById: (request: any, id: string) => Promise<T | null>; fetchById: (request: any, instanceId: string, id: string) => Promise<T | null>;
create: (request: any, data: Partial<T>) => Promise<T>; create: (request: any, instanceId: string, data: Partial<T>) => Promise<T>;
update: (request: any, id: string, data: Partial<T>) => Promise<T>; update: (request: any, instanceId: string, id: string, data: Partial<T>) => Promise<T>;
deleteItem: (request: any, id: string) => Promise<void>; deleteItem: (request: any, instanceId: string, id: string) => Promise<void>;
} }
function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntityConfig<T>) { function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntityConfig<T>) {
return function useTrusteeEntity() { return function useTrusteeEntity() {
// Hole instanceId aus URL-Kontext
const instanceId = useInstanceId();
const [items, setItems] = useState<T[]>([]); const [items, setItems] = useState<T[]>([]);
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]); const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [permissions, setPermissions] = useState<UserPermissions | null>(null); const [permissions, setPermissions] = useState<UserPermissions | null>(null);
@ -116,8 +127,10 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
const { checkPermission } = usePermissions(); const { checkPermission } = usePermissions();
const fetchAttributes = useCallback(async () => { const fetchAttributes = useCallback(async () => {
if (!instanceId) return [];
try { try {
const response = await api.get(`/api/attributes/${config.entityName}`); const response = await api.get(`/api/trustee/${instanceId}/attributes/${config.entityName}`);
let attrs: AttributeDefinition[] = []; let attrs: AttributeDefinition[] = [];
if (response.data?.attributes && Array.isArray(response.data.attributes)) { if (response.data?.attributes && Array.isArray(response.data.attributes)) {
attrs = response.data.attributes; attrs = response.data.attributes;
@ -131,7 +144,7 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
setAttributes([]); setAttributes([]);
return []; return [];
} }
}, []); }, [instanceId]);
const fetchPermissions = useCallback(async () => { const fetchPermissions = useCallback(async () => {
try { try {
@ -153,8 +166,13 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
}, [checkPermission]); }, [checkPermission]);
const fetchItems = useCallback(async (params?: PaginationParams) => { const fetchItems = useCallback(async (params?: PaginationParams) => {
if (!instanceId) {
setItems([]);
return;
}
try { try {
const data = await config.fetchAll(request, params); const data = await config.fetchAll(request, instanceId, params);
if (data && typeof data === 'object' && 'items' in data) { if (data && typeof data === 'object' && 'items' in data) {
const fetchedItems = Array.isArray(data.items) ? data.items : []; const fetchedItems = Array.isArray(data.items) ? data.items : [];
@ -171,7 +189,7 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
setItems([]); setItems([]);
setPagination(null); setPagination(null);
} }
}, [request]); }, [request, instanceId]);
const removeOptimistically = (itemId: string) => { const removeOptimistically = (itemId: string) => {
setItems(prev => prev.filter(item => item.id !== itemId)); setItems(prev => prev.filter(item => item.id !== itemId));
@ -188,8 +206,9 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
}; };
const fetchById = useCallback(async (itemId: string): Promise<T | null> => { const fetchById = useCallback(async (itemId: string): Promise<T | null> => {
return await config.fetchById(request, itemId); if (!instanceId) return null;
}, [request]); return await config.fetchById(request, instanceId, itemId);
}, [request, instanceId]);
const generateEditFieldsFromAttributes = useCallback(() => { const generateEditFieldsFromAttributes = useCallback(() => {
if (!attributes || attributes.length === 0) { if (!attributes || attributes.length === 0) {
@ -198,11 +217,9 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
return attributes return attributes
.filter(attr => { .filter(attr => {
// For EDIT mode: filter out readonly fields and system fields
if (attr.readonly === true || attr.editable === false) { if (attr.readonly === true || attr.editable === false) {
return false; return false;
} }
// Also filter out 'id' for edit mode (id cannot be changed)
if (attr.name === 'id') { if (attr.name === 'id') {
return false; return false;
} }
@ -265,7 +282,6 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
}); });
}, [attributes]); }, [attributes]);
// Generate fields for CREATE forms - includes all required fields like 'id'
const generateCreateFieldsFromAttributes = useCallback(() => { const generateCreateFieldsFromAttributes = useCallback(() => {
if (!attributes || attributes.length === 0) { if (!attributes || attributes.length === 0) {
return []; return [];
@ -273,8 +289,6 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
return attributes return attributes
.filter(attr => { .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']; const systemFields = ['_createdBy', '_createdAt', '_modifiedBy', '_modifiedAt', 'mandateId'];
return !systemFields.includes(attr.name); return !systemFields.includes(attr.name);
}) })
@ -325,7 +339,7 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
key: attr.name, key: attr.name,
label: attr.label || attr.name, label: attr.label || attr.name,
type: fieldType, type: fieldType,
editable: true, // All fields are editable in create mode editable: true,
required: attr.required === true, required: attr.required === true,
options, options,
optionsReference, optionsReference,
@ -341,14 +355,14 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
return await fetchAttributes(); return await fetchAttributes();
}, [attributes, fetchAttributes]); }, [attributes, fetchAttributes]);
// Lade Daten wenn instanceId verfügbar
useEffect(() => { useEffect(() => {
if (instanceId) {
fetchAttributes(); fetchAttributes();
fetchPermissions(); fetchPermissions();
}, [fetchAttributes, fetchPermissions]);
useEffect(() => {
fetchItems(); fetchItems();
}, [fetchItems]); }
}, [instanceId, fetchAttributes, fetchPermissions, fetchItems]);
return { return {
items, items,
@ -363,13 +377,17 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
fetchById, fetchById,
generateEditFieldsFromAttributes, generateEditFieldsFromAttributes,
generateCreateFieldsFromAttributes, generateCreateFieldsFromAttributes,
ensureAttributesLoaded ensureAttributesLoaded,
instanceId // Auch instanceId zurückgeben für Operations-Hook
}; };
}; };
} }
function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeEntityConfig<T>) { function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeEntityConfig<T>) {
return function useTrusteeEntityOperations() { return function useTrusteeEntityOperations() {
// Hole instanceId aus URL-Kontext
const instanceId = useInstanceId();
const [deletingItems, setDeletingItems] = useState<Set<string>>(new Set()); const [deletingItems, setDeletingItems] = useState<Set<string>>(new Set());
const [creatingItem, setCreatingItem] = useState(false); const [creatingItem, setCreatingItem] = useState(false);
const { request, isLoading } = useApiRequest(); const { request, isLoading } = useApiRequest();
@ -377,12 +395,17 @@ function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeE
const [createError, setCreateError] = useState<string | null>(null); const [createError, setCreateError] = useState<string | null>(null);
const [updateError, setUpdateError] = useState<string | null>(null); const [updateError, setUpdateError] = useState<string | null>(null);
const handleDelete = async (itemId: string) => { const handleDelete = useCallback(async (itemId: string) => {
if (!instanceId) {
setDeleteError('No instance context');
return false;
}
setDeleteError(null); setDeleteError(null);
setDeletingItems(prev => new Set(prev).add(itemId)); setDeletingItems(prev => new Set(prev).add(itemId));
try { try {
await config.deleteItem(request, itemId); await config.deleteItem(request, instanceId, itemId);
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise(resolve => setTimeout(resolve, 300));
return true; return true;
} catch (error: any) { } catch (error: any) {
@ -395,20 +418,23 @@ function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeE
return newSet; return newSet;
}); });
} }
}; }, [request, instanceId]);
const handleCreate = useCallback(async (itemData: Partial<T>) => {
if (!instanceId) {
setCreateError('No instance context');
return { success: false, error: 'No instance context' };
}
const handleCreate = async (itemData: Partial<T>) => {
setCreateError(null); setCreateError(null);
setCreatingItem(true); setCreatingItem(true);
// Debug: Log what data is being sent to the backend
console.warn('🔧 handleCreate called with itemData:', itemData); console.warn('🔧 handleCreate called with itemData:', itemData);
try { try {
const newItem = await config.create(request, itemData); const newItem = await config.create(request, instanceId, itemData);
return { success: true, data: newItem }; return { success: true, data: newItem };
} catch (error: any) { } catch (error: any) {
// Debug: Log full error details
console.error('🔧 handleCreate error:', { console.error('🔧 handleCreate error:', {
message: error.message, message: error.message,
response: error.response?.data, response: error.response?.data,
@ -420,13 +446,18 @@ function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeE
} finally { } finally {
setCreatingItem(false); setCreatingItem(false);
} }
}; }, [request, instanceId]);
const handleUpdate = useCallback(async (itemId: string, updateData: Partial<T>) => {
if (!instanceId) {
setUpdateError('No instance context');
return { success: false, error: 'No instance context' };
}
const handleUpdate = async (itemId: string, updateData: Partial<T>) => {
setUpdateError(null); setUpdateError(null);
try { try {
const updatedItem = await config.update(request, itemId, updateData); const updatedItem = await config.update(request, instanceId, itemId, updateData);
return { success: true, data: updatedItem }; return { success: true, data: updatedItem };
} catch (error: any) { } catch (error: any) {
const errorMessage = error.response?.data?.message || error.message || 'Failed to update'; const errorMessage = error.response?.data?.message || error.message || 'Failed to update';
@ -439,7 +470,7 @@ function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeE
isValidationError: error.response?.status === 400 isValidationError: error.response?.status === 400
}; };
} }
}; }, [request, instanceId]);
return { return {
deletingItems, deletingItems,
@ -450,7 +481,8 @@ function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeE
handleDelete, handleDelete,
handleCreate, handleCreate,
handleUpdate, handleUpdate,
isLoading isLoading,
instanceId
}; };
}; };
} }
@ -558,7 +590,7 @@ export const useTrusteePositionOperations = _createTrusteeOperationsHook(positio
const positionDocumentConfig: TrusteeEntityConfig<TrusteePositionDocument> = { const positionDocumentConfig: TrusteeEntityConfig<TrusteePositionDocument> = {
entityName: 'TrusteePositionDocument', entityName: 'TrusteePositionDocument',
fetchAll: fetchPositionDocumentsApi, fetchAll: fetchPositionDocumentsApi,
fetchById: async () => null, // Not typically needed fetchById: async () => null,
create: createPositionDocumentApi, create: createPositionDocumentApi,
update: async () => { throw new Error('Update not supported for position-document links'); }, update: async () => { throw new Error('Update not supported for position-document links'); },
deleteItem: deletePositionDocumentApi deleteItem: deletePositionDocumentApi

View file

@ -31,26 +31,15 @@ export function useCurrentUser() {
// Check if we already have user data in sessionStorage cache // Check if we already have user data in sessionStorage cache
const cachedUser = getUserDataCache(); const cachedUser = getUserDataCache();
if (cachedUser && cachedUser.username) { 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 // 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); setUser(cachedUser);
console.log('✅ Using cached user data from sessionStorage (persists during session):', { console.log('✅ Using cached user data from sessionStorage (persists during session):', {
username: cachedUser.username, username: cachedUser.username,
roleLabels: cachedUser.roleLabels isSysAdmin: cachedUser.isSysAdmin
}); });
return; return;
} }
}
// JWT tokens are now stored in httpOnly cookies, so we fetch user data from API // JWT tokens are now stored in httpOnly cookies, so we fetch user data from API
console.log('🍪 JWT tokens are in httpOnly cookies, fetching user data from API'); console.log('🍪 JWT tokens are in httpOnly cookies, fetching user data from API');
@ -79,49 +68,28 @@ export function useCurrentUser() {
const data = await fetchCurrentUserApi(request, authAuthority || undefined); const data = await fetchCurrentUserApi(request, authAuthority || undefined);
// Log full response for debugging // Log response for debugging
console.log('📦 User data received from API:', { console.log('📦 User data received from API:', {
username: data?.username, username: data?.username,
roleLabels: data?.roleLabels, isSysAdmin: data?.isSysAdmin,
hasRoleLabels: !!data?.roleLabels, allKeys: data ? Object.keys(data) : []
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)
}); });
// Always cache user data - permissions are checked via RBAC API, not client-side // Validate user data
// roleLabels are optional metadata for display/logging purposes
if (!data || !data.username) { if (!data || !data.username) {
console.error('❌ User data from API is invalid:', { console.error('❌ User data from API is invalid:', {
username: data?.username, username: data?.username,
dataKeys: data ? Object.keys(data) : [], dataKeys: data ? Object.keys(data) : []
fullResponse: data
}); });
throw new Error('Invalid user data received from API'); 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) // Cache user data (permissions are checked via RBAC API)
// Note: roleLabels is deprecated - use isSysAdmin flag for admin checks
setUserDataCache(data); 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, username: data.username,
roleLabels: data.roleLabels, isSysAdmin: data.isSysAdmin
roleLabelsLength: Array.isArray(data.roleLabels) ? data.roleLabels.length : 0,
hasRoleLabels
}); });
setUser(data); setUser(data);
} catch (error: any) { } catch (error: any) {
@ -292,25 +260,12 @@ export function useCurrentUser() {
// Try to load user from sessionStorage cache first for faster initial load // Try to load user from sessionStorage cache first for faster initial load
const cachedUser = getUserDataCache(); const cachedUser = getUserDataCache();
if (cachedUser && cachedUser.username) { 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 // Use cached user data - permissions are checked via RBAC API
// Note: roleLabels is deprecated in Multi-Tenant architecture - use isSysAdmin flag instead
setUser(cachedUser); 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, username: cachedUser.username,
roleLabels: cachedUser.roleLabels isSysAdmin: cachedUser.isSysAdmin
}); });
} }
@ -835,20 +790,14 @@ export function useUserOperations() {
} }
}; };
const handleUserCreate = async (userData: Omit<User, 'id' | 'mandateId'>) => { const handleUserCreate = async (userData: Omit<User, 'id'>) => {
setCreateError(null); setCreateError(null);
setCreatingUser(true); setCreatingUser(true);
try { try {
const currentUserData = getUserDataCache(); // mandateId wird nicht mehr vom Client gesendet
const mandateId = currentUserData?.mandateId || ''; // Das Backend bestimmt den Kontext über die instanceId im Request
const newUser = await createUserApi(request, userData);
const requestBody = {
mandateId: mandateId,
...userData
};
const newUser = await createUserApi(request, requestBody);
return { success: true, userData: newUser }; return { success: true, userData: newUser };
} catch (error: any) { } catch (error: any) {
@ -889,15 +838,8 @@ export function useUserOperations() {
setEditingUsers(prev => new Set(prev).add(userId)); setEditingUsers(prev => new Set(prev).add(userId));
try { try {
const currentUserData = getUserDataCache(); // mandateId wird nicht mehr vom Client gesendet
const mandateId = currentUserData?.mandateId || ''; const updatedUser = await updateUserApi(request, userId, updateData);
const requestBody = {
mandateId: mandateId,
...updateData
};
const updatedUser = await updateUserApi(request, userId, requestBody);
return { success: true, userData: updatedUser }; return { success: true, userData: updatedUser };
} catch (error: any) { } catch (error: any) {

View file

@ -8,6 +8,7 @@ html, body {
padding: 0; padding: 0;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
font-family: var(--font-family, "DM Sans", sans-serif);
} }
#root { #root {
@ -15,4 +16,5 @@ html, body {
width: 100vw; width: 100vw;
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: var(--font-family, "DM Sans", sans-serif);
} }

View file

@ -31,6 +31,12 @@
border-bottom: 1px solid var(--border-color, #e0e0e0); border-bottom: 1px solid var(--border-color, #e0e0e0);
} }
.logoImage {
height: 40px;
width: auto;
object-fit: contain;
}
.logoText { .logoText {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
@ -93,6 +99,10 @@
border-bottom-color: var(--border-dark, #333); border-bottom-color: var(--border-dark, #333);
} }
:global(.dark-theme) .logoImage {
filter: brightness(0) invert(1);
}
:global(.dark-theme) .logoPower { :global(.dark-theme) .logoPower {
color: var(--text-primary-dark, #ffffff); color: var(--text-primary-dark, #ffffff);
} }

View file

@ -9,6 +9,7 @@ import React, { useEffect } from 'react';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { FeatureProvider, useFeatureStore } from '../stores/featureStore'; import { FeatureProvider, useFeatureStore } from '../stores/featureStore';
import { MandateNavigation } from '../components/Navigation/MandateNavigation'; import { MandateNavigation } from '../components/Navigation/MandateNavigation';
import { UserSection } from '../components/Navigation/UserSection';
import styles from './MainLayout.module.css'; import styles from './MainLayout.module.css';
// ============================================================================= // =============================================================================
@ -30,10 +31,11 @@ const MainLayoutInner: React.FC = () => {
{/* Sidebar */} {/* Sidebar */}
<aside className={styles.sidebar}> <aside className={styles.sidebar}>
<div className={styles.logoContainer}> <div className={styles.logoContainer}>
<div className={styles.logoText}> <img
<span className={styles.logoPower}>Power</span> src="/logos/poweron-logo.png"
<span className={styles.logoOn}>On</span> alt="PowerOn"
</div> className={styles.logoImage}
/>
</div> </div>
<nav className={styles.navigation}> <nav className={styles.navigation}>
@ -55,9 +57,7 @@ const MainLayoutInner: React.FC = () => {
</nav> </nav>
{/* User-Bereich am unteren Rand */} {/* User-Bereich am unteren Rand */}
<div className={styles.userSection}> <UserSection />
{/* TODO: User-Info Komponente */}
</div>
</aside> </aside>
{/* Content */} {/* Content */}

View file

@ -6,10 +6,11 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
overflow: hidden;
} }
/* View Header */
.viewHeader { .viewHeader {
flex-shrink: 0;
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color, #e0e0e0); border-bottom: 1px solid var(--border-color, #e0e0e0);
background: var(--bg-primary, #ffffff); background: var(--bg-primary, #ffffff);
@ -22,38 +23,33 @@
color: var(--text-primary, #1a1a1a); color: var(--text-primary, #1a1a1a);
} }
/* View Content */
.viewContent { .viewContent {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: 1.5rem; padding: 1.5rem;
} }
/* Placeholder */ /* Placeholder View */
.placeholder { .placeholder {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 300px; min-height: 400px;
padding: 2rem;
background: var(--surface-color, #f8f9fa);
border: 2px dashed var(--border-color, #e0e0e0);
border-radius: 12px;
text-align: center; text-align: center;
padding: 2rem;
} }
.placeholder h2 { .placeholder h2 {
margin: 0; margin: 0 0 0.5rem;
font-size: 1.25rem; font-size: 1.5rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1a1a); color: var(--text-primary, #1a1a1a);
} }
.placeholder p { .placeholder p {
margin: 0.5rem 0 0; margin: 0;
color: var(--text-secondary, #666); color: var(--text-secondary, #666);
font-size: 0.9375rem;
} }
/* Not Found */ /* Not Found */
@ -63,29 +59,22 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 300px; min-height: 400px;
padding: 2rem;
text-align: center; text-align: center;
padding: 2rem;
} }
.notFound h2, .notFound h2,
.accessDenied h2 { .accessDenied h2 {
margin: 0; margin: 0 0 0.5rem;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 600; font-weight: 600;
color: var(--text-primary, #1a1a1a);
} }
.notFound p, .notFound p,
.accessDenied p { .accessDenied p {
margin: 0.5rem 0 0; margin: 0;
color: var(--text-secondary, #666); color: var(--text-secondary, #666);
font-size: 0.9375rem;
}
.accessDenied {
background: var(--error-light, #fef2f2);
border-radius: 12px;
} }
.accessDenied h2 { .accessDenied h2 {
@ -98,25 +87,14 @@
border-bottom-color: var(--border-dark, #333); border-bottom-color: var(--border-dark, #333);
} }
:global(.dark-theme) .viewTitle { :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) .placeholder h2, :global(.dark-theme) .placeholder h2,
:global(.dark-theme) .notFound h2 { :global(.dark-theme) .notFound h2 {
color: var(--text-primary-dark, #ffffff); color: var(--text-primary-dark, #ffffff);
} }
:global(.dark-theme) .placeholder p, :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); color: var(--text-secondary-dark, #aaa);
} }
:global(.dark-theme) .accessDenied {
background: rgba(220, 38, 38, 0.1);
}

View file

@ -3,105 +3,55 @@
* *
* Generische Feature-View-Komponente. * Generische Feature-View-Komponente.
* Rendert den entsprechenden Content basierend auf Feature-Code und View. * 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 React from 'react';
import { useCurrentInstance } from '../hooks/useCurrentInstance'; import { useCurrentInstance } from '../hooks/useCurrentInstance';
import { useCanViewFeatureView } from '../hooks/useInstancePermissions'; import { useCanViewFeatureView } from '../hooks/useInstancePermissions';
import { getLabel, FEATURE_REGISTRY } from '../types/mandate'; 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'; 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 PlaceholderView: React.FC<{ title: string; description: string }> = ({ title, description }) => (
const TrusteeDashboard: React.FC = () => (
<div className={styles.placeholder}> <div className={styles.placeholder}>
<h2>Trustee Dashboard</h2> <h2>{title}</h2>
<p>Übersicht der Treuhand-Aktivitäten</p> <p>{description}</p>
</div>
);
const TrusteeOrganisations: React.FC = () => (
<div className={styles.placeholder}>
<h2>Organisationen</h2>
<p>Verwaltung der Organisationen</p>
</div>
);
const TrusteeContracts: React.FC = () => (
<div className={styles.placeholder}>
<h2>Verträge</h2>
<p>Vertragsverwaltung</p>
</div>
);
const TrusteeDocuments: React.FC = () => (
<div className={styles.placeholder}>
<h2>Dokumente</h2>
<p>Dokumentenverwaltung</p>
</div>
);
const TrusteePositions: React.FC = () => (
<div className={styles.placeholder}>
<h2>Positionen</h2>
<p>Positionsverwaltung</p>
</div>
);
const TrusteeRoles: React.FC = () => (
<div className={styles.placeholder}>
<h2>Rollen</h2>
<p>Rollenverwaltung</p>
</div>
);
const TrusteeAccess: React.FC = () => (
<div className={styles.placeholder}>
<h2>Zugriffe</h2>
<p>Zugriffsverwaltung</p>
</div> </div>
); );
// Chatworkflow Views // Chatworkflow Views
const ChatworkflowDashboard: React.FC = () => ( const ChatworkflowDashboard: React.FC = () => (
<div className={styles.placeholder}> <PlaceholderView title="Workflow Dashboard" description="Übersicht der Workflows" />
<h2>Workflow Dashboard</h2>
<p>Übersicht der Workflows</p>
</div>
); );
const ChatworkflowRuns: React.FC = () => ( const ChatworkflowRuns: React.FC = () => (
<div className={styles.placeholder}> <PlaceholderView title="Runs" description="Workflow-Ausführungen" />
<h2>Runs</h2>
<p>Workflow-Ausführungen</p>
</div>
); );
const ChatworkflowFiles: React.FC = () => ( const ChatworkflowFiles: React.FC = () => (
<div className={styles.placeholder}> <PlaceholderView title="Dateien" description="Workflow-Dateien" />
<h2>Dateien</h2>
<p>Workflow-Dateien</p>
</div>
); );
// Chatbot Views // Chatbot Views
const ChatbotConversations: React.FC = () => ( const ChatbotConversations: React.FC = () => (
<div className={styles.placeholder}> <PlaceholderView title="Konversationen" description="Chat-Konversationen" />
<h2>Konversationen</h2>
<p>Chat-Konversationen</p>
</div>
); );
const ChatbotSettings: React.FC = () => ( const ChatbotSettings: React.FC = () => (
<div className={styles.placeholder}> <PlaceholderView title="Chatbot Einstellungen" description="Konfiguration des Chatbots" />
<h2>Chatbot Einstellungen</h2>
<p>Konfiguration des Chatbots</p>
</div>
); );
// Generic/Fallback // Generic/Fallback
@ -127,13 +77,13 @@ type ViewComponent = React.FC;
const VIEW_COMPONENTS: Record<string, Record<string, ViewComponent>> = { const VIEW_COMPONENTS: Record<string, Record<string, ViewComponent>> = {
trustee: { trustee: {
dashboard: TrusteeDashboard, dashboard: TrusteeDashboardView,
organisations: TrusteeOrganisations, organisations: TrusteeOrganisationsView,
contracts: TrusteeContracts, contracts: TrusteeContractsView,
documents: TrusteeDocuments, documents: TrusteeDocumentsView,
positions: TrusteePositions, positions: TrusteePositionsView,
roles: TrusteeRoles, roles: TrusteeRolesView,
access: TrusteeAccess, access: TrusteeAccessView,
}, },
chatworkflow: { chatworkflow: {
dashboard: ChatworkflowDashboard, dashboard: ChatworkflowDashboard,

View file

@ -13,7 +13,7 @@ import styles from './Settings.module.css';
// ============================================================================= // =============================================================================
export const SettingsPage: React.FC = () => { export const SettingsPage: React.FC = () => {
const { t, language, setLanguage } = useLanguage(); const { currentLanguage, setLanguage } = useLanguage();
const [theme, setTheme] = useState<'light' | 'dark'>( const [theme, setTheme] = useState<'light' | 'dark'>(
() => (localStorage.getItem('theme') as 'light' | 'dark') || 'light' () => (localStorage.getItem('theme') as 'light' | 'dark') || 'light'
); );
@ -79,7 +79,7 @@ export const SettingsPage: React.FC = () => {
<div className={styles.settingControl}> <div className={styles.settingControl}>
<select <select
className={styles.select} className={styles.select}
value={language} value={currentLanguage}
onChange={(e) => setLanguage(e.target.value as 'de' | 'en' | 'fr')} onChange={(e) => setLanguage(e.target.value as 'de' | 'en' | 'fr')}
> >
<option value="de">Deutsch</option> <option value="de">Deutsch</option>

View file

@ -0,0 +1,286 @@
/**
* Admin Pages Styles
*
* Common styles for all admin pages using FormGeneratorTable
*/
.adminPage {
padding: 1.5rem;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.pageHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-shrink: 0;
}
.pageTitle {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.pageSubtitle {
font-size: 0.875rem;
color: var(--text-secondary);
margin: 0.25rem 0 0 0;
}
.headerActions {
display: flex;
gap: 0.75rem;
}
.primaryButton {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--primary-color, #f25843);
color: white;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
}
.primaryButton:hover {
background: var(--primary-dark, #d94d3a);
}
.primaryButton:active {
transform: scale(0.98);
}
.primaryButton:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.secondaryButton {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--surface-color);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
.secondaryButton:hover {
background: var(--bg-secondary);
border-color: var(--text-secondary);
}
.tableContainer {
flex: 1;
min-height: 0;
overflow: hidden;
background: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: 8px;
}
.loadingContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: var(--text-secondary);
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--border-color);
border-top-color: var(--primary-color, #f25843);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.errorContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: var(--danger-color, #e53e3e);
text-align: center;
}
.errorIcon {
font-size: 2rem;
margin-bottom: 0.75rem;
}
.errorMessage {
margin: 0 0 1rem 0;
}
.emptyState {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
color: var(--text-secondary);
text-align: center;
}
.emptyIcon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.emptyTitle {
font-size: 1.125rem;
font-weight: 500;
color: var(--text-primary);
margin: 0 0 0.5rem 0;
}
.emptyDescription {
margin: 0;
max-width: 400px;
}
/* Modal Styles */
.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;
}
.modal {
background: var(--surface-color);
border-radius: 12px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.modalHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.modalTitle {
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.modalClose {
background: none;
border: none;
font-size: 1.25rem;
color: var(--text-secondary);
cursor: pointer;
padding: 0.25rem;
line-height: 1;
transition: color 0.2s;
}
.modalClose:hover {
color: var(--text-primary);
}
.modalContent {
padding: 1.5rem;
overflow-y: auto;
flex: 1;
}
.modalFooter {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding: 1rem 1.5rem;
border-top: 1px solid var(--border-color);
}
/* Form Styles */
.formGroup {
margin-bottom: 1.25rem;
}
.formLabel {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.formInput {
width: 100%;
padding: 0.625rem 0.75rem;
font-size: 0.875rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
transition: border-color 0.2s, box-shadow 0.2s;
}
.formInput:focus {
outline: none;
border-color: var(--primary-color, #f25843);
box-shadow: 0 0 0 3px rgba(242, 88, 67, 0.1);
}
.formInput::placeholder {
color: var(--text-tertiary);
}
.required::after {
content: " *";
color: var(--danger-color, #e53e3e);
}
/* Dark theme adjustments */
:global(.dark-theme) .modal {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
:global(.dark-theme) .modalOverlay {
background: rgba(0, 0, 0, 0.7);
}

View file

@ -0,0 +1,246 @@
/**
* AdminMandatesPage
*
* Admin page for managing Mandates (tenants) using FormGeneratorTable.
*/
import React, { useState } from 'react';
import { useAdminMandates, type Mandate } from '../../hooks/useMandates';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaBuilding } from 'react-icons/fa';
import styles from './Admin.module.css';
export const AdminMandatesPage: React.FC = () => {
const {
mandates,
columns,
permissions,
pagination,
loading,
error,
refetch,
fetchMandateById,
handleCreate,
handleUpdate,
handleDelete,
handleInlineUpdate,
updateOptimistically,
} = useAdminMandates();
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingMandate, setEditingMandate] = useState<Mandate | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Check if user can create
const canCreate = permissions?.create !== 'n';
const canUpdate = permissions?.update !== 'n';
const canDelete = permissions?.delete !== 'n';
// Handle edit click
const handleEditClick = async (mandate: Mandate) => {
const fullMandate = await fetchMandateById(mandate.id);
if (fullMandate) {
setEditingMandate(fullMandate);
}
};
// Handle create submit
const handleCreateSubmit = async (data: Partial<Mandate>) => {
setIsSubmitting(true);
try {
const success = await handleCreate(data);
if (success) {
setShowCreateModal(false);
}
} finally {
setIsSubmitting(false);
}
};
// Handle edit submit
const handleEditSubmit = async (data: Partial<Mandate>) => {
if (!editingMandate) return;
setIsSubmitting(true);
try {
const success = await handleUpdate(editingMandate.id, data);
if (success) {
setEditingMandate(null);
}
} finally {
setIsSubmitting(false);
}
};
// Handle delete
const handleDeleteMandate = async (mandate: Mandate) => {
if (window.confirm(`Möchten Sie den Mandanten "${mandate.name || mandate.id}" wirklich löschen?`)) {
await handleDelete(mandate.id);
}
};
if (error) {
return (
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>Fehler beim Laden der Mandanten: {error}</p>
<button className={styles.secondaryButton} onClick={() => refetch()}>
<FaSync /> Erneut versuchen
</button>
</div>
</div>
);
}
return (
<div className={styles.adminPage}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Mandanten</h1>
<p className={styles.pageSubtitle}>Verwalten Sie alle Mandanten im System</p>
</div>
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={() => refetch()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
</button>
{canCreate && (
<button
className={styles.primaryButton}
onClick={() => setShowCreateModal(true)}
>
<FaPlus /> Neuer Mandant
</button>
)}
</div>
</div>
<div className={styles.tableContainer}>
{loading && mandates.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Mandanten...</span>
</div>
) : mandates.length === 0 ? (
<div className={styles.emptyState}>
<FaBuilding className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>Keine Mandanten vorhanden</h3>
<p className={styles.emptyDescription}>
Erstellen Sie einen neuen Mandanten, um loszulegen.
</p>
{canCreate && (
<button
className={styles.primaryButton}
onClick={() => setShowCreateModal(true)}
>
<FaPlus /> Ersten Mandanten erstellen
</button>
)}
</div>
) : (
<FormGeneratorTable
data={mandates}
columns={columns}
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={false}
actionButtons={[
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: 'Bearbeiten',
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: 'Löschen',
}] : []),
]}
onDelete={handleDeleteMandate}
hookData={{
refetch,
permissions,
pagination,
handleDelete,
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage="Keine Mandanten gefunden"
/>
)}
</div>
{/* Create Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Neuer Mandant</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
>
</button>
</div>
<div className={styles.modalContent}>
<FormGeneratorForm
fields={[
{ key: 'name', label: 'Name', type: 'string', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea' },
{ key: 'enabled', label: 'Aktiv', type: 'boolean' },
]}
onSubmit={handleCreateSubmit}
onCancel={() => setShowCreateModal(false)}
submitLabel="Erstellen"
cancelLabel="Abbrechen"
loading={isSubmitting}
/>
</div>
</div>
</div>
)}
{/* Edit Modal */}
{editingMandate && (
<div className={styles.modalOverlay} onClick={() => setEditingMandate(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Mandant bearbeiten</h2>
<button
className={styles.modalClose}
onClick={() => setEditingMandate(null)}
>
</button>
</div>
<div className={styles.modalContent}>
<FormGeneratorForm
fields={[
{ key: 'name', label: 'Name', type: 'string', required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea' },
{ key: 'enabled', label: 'Aktiv', type: 'boolean' },
]}
initialData={editingMandate}
onSubmit={handleEditSubmit}
onCancel={() => setEditingMandate(null)}
submitLabel="Speichern"
cancelLabel="Abbrechen"
loading={isSubmitting}
/>
</div>
</div>
</div>
)}
</div>
);
};
export default AdminMandatesPage;

View file

@ -0,0 +1,277 @@
/**
* AdminRolesPage
*
* Admin page for managing global RBAC Roles using FormGeneratorTable.
*/
import React, { useState, useMemo } from 'react';
import { useAdminRoles, type Role } from '../../hooks/useRoles';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaUserShield } from 'react-icons/fa';
import styles from './Admin.module.css';
export const AdminRolesPage: React.FC = () => {
const {
roles,
columns,
permissions,
pagination,
loading,
error,
refetch,
fetchRoleById,
handleCreate,
handleUpdate,
handleDelete,
handleInlineUpdate,
updateOptimistically,
} = useAdminRoles();
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingRole, setEditingRole] = useState<Role | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Ensure columns have at least default values
const displayColumns = useMemo(() => {
if (columns.length > 0) return columns;
// Default columns if none from backend
return [
{ key: 'roleLabel', label: 'Rollen-Label', type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 150 },
{ key: 'description', label: 'Beschreibung', type: 'string' as const, sortable: true, filterable: false, searchable: true, width: 300 },
];
}, [columns]);
// Check permissions
const canCreate = permissions?.create !== 'n';
const canUpdate = permissions?.update !== 'n';
const canDelete = permissions?.delete !== 'n';
// Handle edit click
const handleEditClick = async (role: Role) => {
const fullRole = await fetchRoleById(role.id);
if (fullRole) {
setEditingRole(fullRole);
}
};
// Handle create submit
const handleCreateSubmit = async (data: Partial<Role>) => {
setIsSubmitting(true);
try {
// Transform description to TextMultilingual format if it's a string
const roleData: Partial<Role> = {
...data,
description: typeof data.description === 'string'
? { en: data.description }
: data.description,
};
const success = await handleCreate(roleData);
if (success) {
setShowCreateModal(false);
}
} finally {
setIsSubmitting(false);
}
};
// Handle edit submit
const handleEditSubmit = async (data: Partial<Role>) => {
if (!editingRole) return;
setIsSubmitting(true);
try {
// Transform description to TextMultilingual format if it's a string
const roleData: Partial<Role> = {
...data,
description: typeof data.description === 'string'
? { en: data.description }
: data.description,
};
const success = await handleUpdate(editingRole.id, roleData);
if (success) {
setEditingRole(null);
}
} finally {
setIsSubmitting(false);
}
};
// Handle delete
const handleDeleteRole = async (role: Role) => {
if (window.confirm(`Möchten Sie die Rolle "${role.roleLabel || role.id}" wirklich löschen?`)) {
await handleDelete(role.id);
}
};
// Form fields for create/edit
const formFields = useMemo(() => [
{ key: 'roleLabel', label: 'Rollen-Label', type: 'string' as const, required: true },
{ key: 'description', label: 'Beschreibung', type: 'textarea' as const },
], []);
if (error) {
return (
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>Fehler beim Laden der Rollen: {error}</p>
<button className={styles.secondaryButton} onClick={() => refetch()}>
<FaSync /> Erneut versuchen
</button>
</div>
</div>
);
}
return (
<div className={styles.adminPage}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Globale Rollen</h1>
<p className={styles.pageSubtitle}>Verwalten Sie die systemweiten RBAC-Rollen</p>
</div>
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={() => refetch()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
</button>
{canCreate && (
<button
className={styles.primaryButton}
onClick={() => setShowCreateModal(true)}
>
<FaPlus /> Neue Rolle
</button>
)}
</div>
</div>
<div className={styles.tableContainer}>
{loading && roles.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Rollen...</span>
</div>
) : roles.length === 0 ? (
<div className={styles.emptyState}>
<FaUserShield className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>Keine Rollen vorhanden</h3>
<p className={styles.emptyDescription}>
Erstellen Sie eine neue Rolle, um Berechtigungen zu definieren.
</p>
{canCreate && (
<button
className={styles.primaryButton}
onClick={() => setShowCreateModal(true)}
>
<FaPlus /> Erste Rolle erstellen
</button>
)}
</div>
) : (
<FormGeneratorTable
data={roles}
columns={displayColumns}
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={false}
actionButtons={[
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: 'Bearbeiten',
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: 'Löschen',
}] : []),
]}
onDelete={handleDeleteRole}
hookData={{
refetch,
permissions,
pagination,
handleDelete,
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage="Keine Rollen gefunden"
/>
)}
</div>
{/* Create Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Neue Rolle</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
>
</button>
</div>
<div className={styles.modalContent}>
<FormGeneratorForm
fields={formFields}
onSubmit={handleCreateSubmit}
onCancel={() => setShowCreateModal(false)}
submitLabel="Erstellen"
cancelLabel="Abbrechen"
loading={isSubmitting}
/>
</div>
</div>
</div>
)}
{/* Edit Modal */}
{editingRole && (
<div className={styles.modalOverlay} onClick={() => setEditingRole(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Rolle bearbeiten</h2>
<button
className={styles.modalClose}
onClick={() => setEditingRole(null)}
>
</button>
</div>
<div className={styles.modalContent}>
<FormGeneratorForm
fields={formFields}
initialData={{
...editingRole,
// Extract description from TextMultilingual if needed
description: typeof editingRole.description === 'object'
? editingRole.description?.en || editingRole.description?.ge || ''
: editingRole.description,
}}
onSubmit={handleEditSubmit}
onCancel={() => setEditingRole(null)}
submitLabel="Speichern"
cancelLabel="Abbrechen"
loading={isSubmitting}
/>
</div>
</div>
</div>
)}
</div>
);
};
export default AdminRolesPage;

View file

@ -0,0 +1,325 @@
/**
* AdminUsersPage
*
* Admin page for managing Users using FormGeneratorTable.
*/
import React, { useState, useMemo } from 'react';
import { useOrgUsers, useUserOperations } from '../../hooks/useUsers';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaUsers, FaKey } from 'react-icons/fa';
import styles from './Admin.module.css';
interface User {
id: string;
username: string;
email: string;
fullName: string;
enabled: boolean;
isSysAdmin?: boolean;
[key: string]: any;
}
export const AdminUsersPage: React.FC = () => {
// Use two hooks: one for data, one for operations
const {
data: users,
attributes,
permissions,
pagination,
loading,
error,
refetch,
fetchUserById,
updateOptimistically,
} = useOrgUsers();
const {
handleUserCreate: createUser,
handleUserUpdate: updateUser,
handleUserDelete: deleteUser,
handleSendPasswordLink,
handleInlineUpdate,
sendingPasswordLink: sendingPasswordLinkState,
} = useUserOperations();
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Generate columns from attributes
const columns = useMemo(() => {
if (!attributes || attributes.length === 0) {
// Default columns if no attributes loaded
return [
{ key: 'username', label: 'Benutzername', type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 150 },
{ key: 'email', label: 'E-Mail', type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 200 },
{ key: 'fullName', label: 'Voller Name', type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 180 },
{ key: 'enabled', label: 'Aktiv', type: 'boolean' as const, sortable: true, filterable: true, width: 80 },
{ key: 'isSysAdmin', label: 'Admin', type: 'boolean' as const, sortable: true, filterable: true, width: 80 },
];
}
return 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,
}));
}, [attributes]);
// Check permissions
const canCreate = permissions?.create !== 'n';
const canUpdate = permissions?.update !== 'n';
const canDelete = permissions?.delete !== 'n';
// Handle edit click
const handleEditClick = async (user: User) => {
const fullUser = await fetchUserById(user.id);
if (fullUser) {
setEditingUser(fullUser as User);
}
};
// Handle create submit
const handleCreateSubmit = async (data: Partial<User>) => {
setIsSubmitting(true);
try {
const result = await createUser(data as Omit<User, 'id'>);
if (result.success) {
setShowCreateModal(false);
refetch(); // Refresh the list
}
} finally {
setIsSubmitting(false);
}
};
// Handle edit submit
const handleEditSubmit = async (data: Partial<User>) => {
if (!editingUser) return;
setIsSubmitting(true);
try {
const result = await updateUser(editingUser.id, data);
if (result.success) {
setEditingUser(null);
refetch(); // Refresh the list
}
} finally {
setIsSubmitting(false);
}
};
// Handle delete
const handleDeleteUser = async (user: User) => {
if (window.confirm(`Möchten Sie den Benutzer "${user.username}" wirklich löschen?`)) {
const success = await deleteUser(user.id);
if (success) {
refetch(); // Refresh the list
}
}
};
// Handle send password link
const handleSendPassword = async (user: User) => {
await handleSendPasswordLink(user.id);
};
// Create form fields (using AttributeDefinition format)
const createFields = useMemo(() => [
{ name: 'username', label: 'Benutzername', type: 'string' as const, required: true },
{ name: 'email', label: 'E-Mail', type: 'email' as const, required: true },
{ name: 'fullName', label: 'Voller Name', type: 'string' as const, required: true },
{ name: 'language', label: 'Sprache', type: 'enum' as const, options: [
{ value: 'de', label: 'Deutsch' },
{ value: 'en', label: 'English' },
]},
{ name: 'enabled', label: 'Aktiv', type: 'boolean' as const },
{ name: 'isSysAdmin', label: 'System-Administrator', type: 'boolean' as const },
], []);
// Edit form fields (using AttributeDefinition format)
const editFields = useMemo(() => [
{ name: 'username', label: 'Benutzername', type: 'string' as const, required: true, editable: false },
{ name: 'email', label: 'E-Mail', type: 'email' as const, required: true },
{ name: 'fullName', label: 'Voller Name', type: 'string' as const, required: true },
{ name: 'language', label: 'Sprache', type: 'enum' as const, options: [
{ value: 'de', label: 'Deutsch' },
{ value: 'en', label: 'English' },
]},
{ name: 'enabled', label: 'Aktiv', type: 'boolean' as const },
{ name: 'isSysAdmin', label: 'System-Administrator', type: 'boolean' as const },
], []);
if (error) {
return (
<div className={styles.adminPage}>
<div className={styles.errorContainer}>
<span className={styles.errorIcon}></span>
<p className={styles.errorMessage}>Fehler beim Laden der Benutzer: {error}</p>
<button className={styles.secondaryButton} onClick={() => refetch()}>
<FaSync /> Erneut versuchen
</button>
</div>
</div>
);
}
return (
<div className={styles.adminPage}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Benutzer</h1>
<p className={styles.pageSubtitle}>Verwalten Sie alle Benutzer im System</p>
</div>
<div className={styles.headerActions}>
<button
className={styles.secondaryButton}
onClick={() => refetch()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
</button>
{canCreate && (
<button
className={styles.primaryButton}
onClick={() => setShowCreateModal(true)}
>
<FaPlus /> Neuer Benutzer
</button>
)}
</div>
</div>
<div className={styles.tableContainer}>
{loading && (!users || users.length === 0) ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Benutzer...</span>
</div>
) : !users || users.length === 0 ? (
<div className={styles.emptyState}>
<FaUsers className={styles.emptyIcon} />
<h3 className={styles.emptyTitle}>Keine Benutzer vorhanden</h3>
<p className={styles.emptyDescription}>
Erstellen Sie einen neuen Benutzer, um loszulegen.
</p>
{canCreate && (
<button
className={styles.primaryButton}
onClick={() => setShowCreateModal(true)}
>
<FaPlus /> Ersten Benutzer erstellen
</button>
)}
</div>
) : (
<FormGeneratorTable
data={users}
columns={columns}
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={false}
actionButtons={[
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: 'Bearbeiten',
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: 'Löschen',
}] : []),
]}
customActions={canUpdate ? [
{
id: 'sendPasswordLink',
icon: <FaKey />,
onClick: handleSendPassword,
title: 'Passwort-Link senden',
loading: (row: User) => sendingPasswordLinkState.has(row.id),
}
] : []}
onDelete={handleDeleteUser}
hookData={{
refetch,
permissions,
pagination,
handleDelete: deleteUser,
handleInlineUpdate,
updateOptimistically,
}}
emptyMessage="Keine Benutzer gefunden"
/>
)}
</div>
{/* Create Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Neuer Benutzer</h2>
<button
className={styles.modalClose}
onClick={() => setShowCreateModal(false)}
>
</button>
</div>
<div className={styles.modalContent}>
<FormGeneratorForm
attributes={createFields}
mode="create"
onSubmit={handleCreateSubmit}
onCancel={() => setShowCreateModal(false)}
submitButtonText="Erstellen"
cancelButtonText="Abbrechen"
/>
</div>
</div>
</div>
)}
{/* Edit Modal */}
{editingUser && (
<div className={styles.modalOverlay} onClick={() => setEditingUser(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Benutzer bearbeiten</h2>
<button
className={styles.modalClose}
onClick={() => setEditingUser(null)}
>
</button>
</div>
<div className={styles.modalContent}>
<FormGeneratorForm
attributes={editFields}
data={editingUser}
mode="edit"
onSubmit={handleEditSubmit}
onCancel={() => setEditingUser(null)}
submitButtonText="Speichern"
cancelButtonText="Abbrechen"
/>
</div>
</div>
</div>
)}
</div>
);
};
export default AdminUsersPage;

9
src/pages/admin/index.ts Normal file
View file

@ -0,0 +1,9 @@
/**
* Admin Pages Index
*
* Export all admin pages for easy importing
*/
export { AdminMandatesPage } from './AdminMandatesPage';
export { AdminUsersPage } from './AdminUsersPage';
export { AdminRolesPage } from './AdminRolesPage';

View file

@ -0,0 +1,97 @@
/**
* TrusteeAccessView
*
* Zugriffs-Verwaltung für eine Trustee-Instanz
*/
import React from 'react';
import { useTrusteeAccess, useTrusteeAccessOperations } from '../../../hooks/useTrustee';
import { useTablePermission } from '../../../hooks/useInstancePermissions';
import styles from './TrusteeViews.module.css';
export const TrusteeAccessView: React.FC = () => {
const { items: accessList, loading, error, refetch } = useTrusteeAccess();
const { handleDelete, deletingItems } = useTrusteeAccessOperations();
const { canCreate, canUpdate, canDelete } = useTablePermission('TrusteeAccess');
if (loading) {
return <div className={styles.loading}>Lade Zugriffe...</div>;
}
if (error) {
return <div className={styles.error}>Fehler: {error}</div>;
}
const onDelete = async (accessId: string) => {
if (window.confirm('Zugriff wirklich entfernen?')) {
const success = await handleDelete(accessId);
if (success) {
refetch();
}
}
};
return (
<div className={styles.listView}>
{/* Toolbar */}
<div className={styles.toolbar}>
{canCreate && (
<button className={styles.primaryButton}>
+ Neuer Zugriff
</button>
)}
<button className={styles.secondaryButton} onClick={() => refetch()}>
Aktualisieren
</button>
</div>
{/* Tabelle */}
{accessList.length === 0 ? (
<div className={styles.emptyState}>
<p>Keine Zugriffe definiert.</p>
</div>
) : (
<table className={styles.dataTable}>
<thead>
<tr>
<th>User</th>
<th>Organisation</th>
<th>Rolle</th>
<th>Vertrag</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{accessList.map((access) => (
<tr key={access.id}>
<td>{access.userId}</td>
<td>{access.organisationId}</td>
<td>{access.roleId}</td>
<td>{access.contractId || '-'}</td>
<td className={styles.actions}>
{canUpdate && (
<button className={styles.iconButton} title="Bearbeiten">
</button>
)}
{canDelete && (
<button
className={styles.iconButton}
title="Entfernen"
onClick={() => onDelete(access.id)}
disabled={deletingItems.has(access.id)}
>
{deletingItems.has(access.id) ? '...' : '🗑️'}
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
};
export default TrusteeAccessView;

View file

@ -0,0 +1,99 @@
/**
* TrusteeContractsView
*
* Vertrags-Verwaltung für eine Trustee-Instanz
*/
import React from 'react';
import { useTrusteeContracts, useTrusteeContractOperations } from '../../../hooks/useTrustee';
import { useTablePermission } from '../../../hooks/useInstancePermissions';
import styles from './TrusteeViews.module.css';
export const TrusteeContractsView: React.FC = () => {
const { items: contracts, loading, error, refetch } = useTrusteeContracts();
const { handleDelete, deletingItems } = useTrusteeContractOperations();
const { canCreate, canUpdate, canDelete } = useTablePermission('TrusteeContract');
if (loading) {
return <div className={styles.loading}>Lade Verträge...</div>;
}
if (error) {
return <div className={styles.error}>Fehler: {error}</div>;
}
const onDelete = async (contractId: string) => {
if (window.confirm('Vertrag wirklich löschen?')) {
const success = await handleDelete(contractId);
if (success) {
refetch();
}
}
};
return (
<div className={styles.listView}>
{/* Toolbar */}
<div className={styles.toolbar}>
{canCreate && (
<button className={styles.primaryButton}>
+ Neuer Vertrag
</button>
)}
<button className={styles.secondaryButton} onClick={() => refetch()}>
Aktualisieren
</button>
</div>
{/* Tabelle */}
{contracts.length === 0 ? (
<div className={styles.emptyState}>
<p>Keine Verträge vorhanden.</p>
</div>
) : (
<table className={styles.dataTable}>
<thead>
<tr>
<th>Label</th>
<th>Organisation</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{contracts.map((contract) => (
<tr key={contract.id}>
<td>{contract.label}</td>
<td>{contract.organisationId}</td>
<td>
<span className={`${styles.badge} ${contract.enabled ? styles.badgeSuccess : styles.badgeWarning}`}>
{contract.enabled ? 'Aktiv' : 'Inaktiv'}
</span>
</td>
<td className={styles.actions}>
{canUpdate && (
<button className={styles.iconButton} title="Bearbeiten">
</button>
)}
{canDelete && (
<button
className={styles.iconButton}
title="Löschen"
onClick={() => onDelete(contract.id)}
disabled={deletingItems.has(contract.id)}
>
{deletingItems.has(contract.id) ? '...' : '🗑️'}
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
};
export default TrusteeContractsView;

View file

@ -0,0 +1,73 @@
/**
* TrusteeDashboardView
*
* Übersicht/Dashboard für eine Trustee-Instanz
*/
import React from 'react';
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
import { useTrusteeOrganisations } from '../../../hooks/useTrustee';
import { useTrusteeContracts } from '../../../hooks/useTrustee';
import styles from './TrusteeViews.module.css';
export const TrusteeDashboardView: React.FC = () => {
const { instance } = useCurrentInstance();
const { items: organisations, loading: orgsLoading } = useTrusteeOrganisations();
const { items: contracts, loading: contractsLoading } = useTrusteeContracts();
const isLoading = orgsLoading || contractsLoading;
return (
<div className={styles.dashboardView}>
<div className={styles.statsGrid}>
{/* Organisationen Card */}
<div className={styles.statCard}>
<div className={styles.statIcon}>🏢</div>
<div className={styles.statContent}>
<div className={styles.statValue}>
{isLoading ? '...' : organisations.length}
</div>
<div className={styles.statLabel}>Organisationen</div>
</div>
</div>
{/* Verträge Card */}
<div className={styles.statCard}>
<div className={styles.statIcon}>📄</div>
<div className={styles.statContent}>
<div className={styles.statValue}>
{isLoading ? '...' : contracts.length}
</div>
<div className={styles.statLabel}>Verträge</div>
</div>
</div>
{/* Rolle Card */}
<div className={styles.statCard}>
<div className={styles.statIcon}>👤</div>
<div className={styles.statContent}>
<div className={styles.statValue}>{instance?.userRole || '-'}</div>
<div className={styles.statLabel}>Deine Rolle</div>
</div>
</div>
</div>
{/* Info-Bereich */}
<div className={styles.infoSection}>
<h3>Instanz-Details</h3>
<div className={styles.infoGrid}>
<div className={styles.infoItem}>
<span className={styles.infoLabel}>Instanz:</span>
<span className={styles.infoValue}>{instance?.instanceLabel}</span>
</div>
<div className={styles.infoItem}>
<span className={styles.infoLabel}>Mandant:</span>
<span className={styles.infoValue}>{instance?.mandateName}</span>
</div>
</div>
</div>
</div>
);
};
export default TrusteeDashboardView;

View file

@ -0,0 +1,98 @@
/**
* TrusteeDocumentsView
*
* Dokument-Verwaltung für eine Trustee-Instanz
*/
import React from 'react';
import { useTrusteeDocuments, useTrusteeDocumentOperations } from '../../../hooks/useTrustee';
import { useTablePermission } from '../../../hooks/useInstancePermissions';
import styles from './TrusteeViews.module.css';
export const TrusteeDocumentsView: React.FC = () => {
const { items: documents, loading, error, refetch } = useTrusteeDocuments();
const { handleDelete, deletingItems } = useTrusteeDocumentOperations();
const { canCreate, canUpdate, canDelete } = useTablePermission('TrusteeDocument');
if (loading) {
return <div className={styles.loading}>Lade Dokumente...</div>;
}
if (error) {
return <div className={styles.error}>Fehler: {error}</div>;
}
const onDelete = async (docId: string) => {
if (window.confirm('Dokument wirklich löschen?')) {
const success = await handleDelete(docId);
if (success) {
refetch();
}
}
};
return (
<div className={styles.listView}>
{/* Toolbar */}
<div className={styles.toolbar}>
{canCreate && (
<button className={styles.primaryButton}>
+ Neues Dokument
</button>
)}
<button className={styles.secondaryButton} onClick={() => refetch()}>
Aktualisieren
</button>
</div>
{/* Tabelle */}
{documents.length === 0 ? (
<div className={styles.emptyState}>
<p>Keine Dokumente vorhanden.</p>
</div>
) : (
<table className={styles.dataTable}>
<thead>
<tr>
<th>Name</th>
<th>Typ</th>
<th>Vertrag</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{documents.map((doc) => (
<tr key={doc.id}>
<td>{doc.documentName}</td>
<td>{doc.documentMimeType}</td>
<td>{doc.contractId}</td>
<td className={styles.actions}>
<button className={styles.iconButton} title="Herunterladen">
</button>
{canUpdate && (
<button className={styles.iconButton} title="Bearbeiten">
</button>
)}
{canDelete && (
<button
className={styles.iconButton}
title="Löschen"
onClick={() => onDelete(doc.id)}
disabled={deletingItems.has(doc.id)}
>
{deletingItems.has(doc.id) ? '...' : '🗑️'}
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
};
export default TrusteeDocumentsView;

View file

@ -0,0 +1,97 @@
/**
* TrusteeOrganisationsView
*
* Organisations-Verwaltung für eine Trustee-Instanz
*/
import React from 'react';
import { useTrusteeOrganisations, useTrusteeOrganisationOperations } from '../../../hooks/useTrustee';
import { useTablePermission } from '../../../hooks/useInstancePermissions';
import styles from './TrusteeViews.module.css';
export const TrusteeOrganisationsView: React.FC = () => {
const { items: organisations, loading, error, refetch } = useTrusteeOrganisations();
const { handleDelete, deletingItems } = useTrusteeOrganisationOperations();
const { canCreate, canUpdate, canDelete } = useTablePermission('TrusteeOrganisation');
if (loading) {
return <div className={styles.loading}>Lade Organisationen...</div>;
}
if (error) {
return <div className={styles.error}>Fehler: {error}</div>;
}
const onDelete = async (orgId: string) => {
if (window.confirm('Organisation wirklich löschen?')) {
const success = await handleDelete(orgId);
if (success) {
refetch();
}
}
};
return (
<div className={styles.listView}>
{/* Toolbar */}
<div className={styles.toolbar}>
{canCreate && (
<button className={styles.primaryButton}>
+ Neue Organisation
</button>
)}
<button className={styles.secondaryButton} onClick={() => refetch()}>
Aktualisieren
</button>
</div>
{/* Tabelle */}
{organisations.length === 0 ? (
<div className={styles.emptyState}>
<p>Keine Organisationen vorhanden.</p>
</div>
) : (
<table className={styles.dataTable}>
<thead>
<tr>
<th>Label</th>
<th>Status</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{organisations.map((org) => (
<tr key={org.id}>
<td>{org.label}</td>
<td>
<span className={`${styles.badge} ${org.enabled ? styles.badgeSuccess : styles.badgeWarning}`}>
{org.enabled ? 'Aktiv' : 'Inaktiv'}
</span>
</td>
<td className={styles.actions}>
{canUpdate && (
<button className={styles.iconButton} title="Bearbeiten">
</button>
)}
{canDelete && (
<button
className={styles.iconButton}
title="Löschen"
onClick={() => onDelete(org.id)}
disabled={deletingItems.has(org.id)}
>
{deletingItems.has(org.id) ? '...' : '🗑️'}
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
};
export default TrusteeOrganisationsView;

View file

@ -0,0 +1,103 @@
/**
* TrusteePositionsView
*
* Positions-Verwaltung für eine Trustee-Instanz
*/
import React from 'react';
import { useTrusteePositions, useTrusteePositionOperations } from '../../../hooks/useTrustee';
import { useTablePermission } from '../../../hooks/useInstancePermissions';
import styles from './TrusteeViews.module.css';
export const TrusteePositionsView: React.FC = () => {
const { items: positions, loading, error, refetch } = useTrusteePositions();
const { handleDelete, deletingItems } = useTrusteePositionOperations();
const { canCreate, canUpdate, canDelete } = useTablePermission('TrusteePosition');
if (loading) {
return <div className={styles.loading}>Lade Positionen...</div>;
}
if (error) {
return <div className={styles.error}>Fehler: {error}</div>;
}
const onDelete = async (posId: string) => {
if (window.confirm('Position wirklich löschen?')) {
const success = await handleDelete(posId);
if (success) {
refetch();
}
}
};
// Formatiere Betrag
const formatAmount = (amount: number, currency: string) => {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: currency || 'CHF'
}).format(amount);
};
return (
<div className={styles.listView}>
{/* Toolbar */}
<div className={styles.toolbar}>
{canCreate && (
<button className={styles.primaryButton}>
+ Neue Position
</button>
)}
<button className={styles.secondaryButton} onClick={() => refetch()}>
Aktualisieren
</button>
</div>
{/* Tabelle */}
{positions.length === 0 ? (
<div className={styles.emptyState}>
<p>Keine Positionen vorhanden.</p>
</div>
) : (
<table className={styles.dataTable}>
<thead>
<tr>
<th>Beschreibung</th>
<th>Firma</th>
<th>Betrag</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{positions.map((pos) => (
<tr key={pos.id}>
<td>{pos.desc}</td>
<td>{pos.company}</td>
<td>{formatAmount(pos.bookingAmount, pos.bookingCurrency)}</td>
<td className={styles.actions}>
{canUpdate && (
<button className={styles.iconButton} title="Bearbeiten">
</button>
)}
{canDelete && (
<button
className={styles.iconButton}
title="Löschen"
onClick={() => onDelete(pos.id)}
disabled={deletingItems.has(pos.id)}
>
{deletingItems.has(pos.id) ? '...' : '🗑️'}
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
};
export default TrusteePositionsView;

View file

@ -0,0 +1,93 @@
/**
* TrusteeRolesView
*
* Rollen-Verwaltung für eine Trustee-Instanz
*/
import React from 'react';
import { useTrusteeRoles, useTrusteeRoleOperations } from '../../../hooks/useTrustee';
import { useTablePermission } from '../../../hooks/useInstancePermissions';
import styles from './TrusteeViews.module.css';
export const TrusteeRolesView: React.FC = () => {
const { items: roles, loading, error, refetch } = useTrusteeRoles();
const { handleDelete, deletingItems } = useTrusteeRoleOperations();
const { canCreate, canUpdate, canDelete } = useTablePermission('TrusteeRole');
if (loading) {
return <div className={styles.loading}>Lade Rollen...</div>;
}
if (error) {
return <div className={styles.error}>Fehler: {error}</div>;
}
const onDelete = async (roleId: string) => {
if (window.confirm('Rolle wirklich löschen?')) {
const success = await handleDelete(roleId);
if (success) {
refetch();
}
}
};
return (
<div className={styles.listView}>
{/* Toolbar */}
<div className={styles.toolbar}>
{canCreate && (
<button className={styles.primaryButton}>
+ Neue Rolle
</button>
)}
<button className={styles.secondaryButton} onClick={() => refetch()}>
Aktualisieren
</button>
</div>
{/* Tabelle */}
{roles.length === 0 ? (
<div className={styles.emptyState}>
<p>Keine Rollen vorhanden.</p>
</div>
) : (
<table className={styles.dataTable}>
<thead>
<tr>
<th>ID</th>
<th>Beschreibung</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{roles.map((role) => (
<tr key={role.id}>
<td><code>{role.id}</code></td>
<td>{role.desc}</td>
<td className={styles.actions}>
{canUpdate && (
<button className={styles.iconButton} title="Bearbeiten">
</button>
)}
{canDelete && (
<button
className={styles.iconButton}
title="Löschen"
onClick={() => onDelete(role.id)}
disabled={deletingItems.has(role.id)}
>
{deletingItems.has(role.id) ? '...' : '🗑️'}
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
};
export default TrusteeRolesView;

View file

@ -0,0 +1,312 @@
/**
* Trustee Views Shared Styles
*/
/* Loading & Error */
.loading,
.error {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
font-size: 0.9375rem;
}
.error {
color: var(--error-color, #dc2626);
}
/* List View */
.listView {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* Toolbar */
.toolbar {
display: flex;
gap: 0.75rem;
padding: 0.5rem 0;
}
.primaryButton {
padding: 0.5rem 1rem;
border: none;
border-radius: 6px;
background: var(--primary-color, #2563eb);
color: white;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.primaryButton:hover {
background: var(--primary-hover, #1d4ed8);
}
.secondaryButton {
padding: 0.5rem 1rem;
border: 1px solid var(--border-color, #d0d0d0);
border-radius: 6px;
background: var(--bg-primary, #ffffff);
color: var(--text-primary, #1a1a1a);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.secondaryButton:hover {
background: var(--surface-color, #f5f5f5);
}
/* Data Table */
.dataTable {
width: 100%;
border-collapse: collapse;
background: var(--bg-primary, #ffffff);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 8px;
overflow: hidden;
}
.dataTable th,
.dataTable td {
padding: 0.75rem 1rem;
text-align: left;
border-bottom: 1px solid var(--border-color, #e0e0e0);
}
.dataTable th {
background: var(--surface-color, #f8f9fa);
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.025em;
color: var(--text-secondary, #666);
}
.dataTable td {
font-size: 0.875rem;
color: var(--text-primary, #1a1a1a);
}
.dataTable tbody tr:last-child td {
border-bottom: none;
}
.dataTable tbody tr:hover {
background: var(--hover-bg, rgba(0, 0, 0, 0.02));
}
/* Actions Column */
.actions {
display: flex;
gap: 0.25rem;
}
.iconButton {
padding: 0.375rem;
border: none;
border-radius: 4px;
background: transparent;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s;
}
.iconButton:hover {
background: var(--hover-bg, rgba(0, 0, 0, 0.05));
}
.iconButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Badge */
.badge {
display: inline-block;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
.badgeSuccess {
background: var(--success-light, #dcfce7);
color: var(--success-color, #16a34a);
}
.badgeWarning {
background: var(--warning-light, #fef9c3);
color: var(--warning-color, #ca8a04);
}
/* Empty State */
.emptyState {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
padding: 2rem;
background: var(--surface-color, #f8f9fa);
border: 2px dashed var(--border-color, #e0e0e0);
border-radius: 8px;
}
.emptyState p {
margin: 0;
color: var(--text-secondary, #666);
font-size: 0.9375rem;
}
/* Dashboard View */
.dashboardView {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.statCard {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.25rem;
background: var(--bg-primary, #ffffff);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 12px;
}
.statIcon {
font-size: 2rem;
}
.statContent {
flex: 1;
}
.statValue {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary, #1a1a1a);
}
.statLabel {
font-size: 0.8125rem;
color: var(--text-secondary, #666);
}
.infoSection {
padding: 1.25rem;
background: var(--bg-primary, #ffffff);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 12px;
}
.infoSection h3 {
margin: 0 0 1rem;
font-size: 1rem;
font-weight: 600;
color: var(--text-primary, #1a1a1a);
}
.infoGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.infoItem {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.infoLabel {
font-size: 0.75rem;
color: var(--text-tertiary, #888);
text-transform: uppercase;
letter-spacing: 0.025em;
}
.infoValue {
font-size: 0.9375rem;
font-weight: 500;
color: var(--text-primary, #1a1a1a);
}
/* Dark Theme */
:global(.dark-theme) .dataTable {
background: var(--surface-dark, #1a1a1a);
border-color: var(--border-dark, #333);
}
:global(.dark-theme) .dataTable th {
background: var(--surface-dark, #2a2a2a);
color: var(--text-secondary-dark, #aaa);
}
:global(.dark-theme) .dataTable th,
:global(.dark-theme) .dataTable td {
border-bottom-color: var(--border-dark, #333);
}
:global(.dark-theme) .dataTable td {
color: var(--text-primary-dark, #ffffff);
}
:global(.dark-theme) .dataTable tbody tr:hover {
background: var(--hover-bg-dark, rgba(255, 255, 255, 0.03));
}
:global(.dark-theme) .secondaryButton {
background: var(--surface-dark, #1a1a1a);
border-color: var(--border-dark, #444);
color: var(--text-primary-dark, #ffffff);
}
:global(.dark-theme) .secondaryButton:hover {
background: var(--surface-dark, #2a2a2a);
}
:global(.dark-theme) .iconButton:hover {
background: var(--hover-bg-dark, rgba(255, 255, 255, 0.1));
}
:global(.dark-theme) .emptyState {
background: var(--surface-dark, #1a1a1a);
border-color: var(--border-dark, #444);
}
:global(.dark-theme) .emptyState p {
color: var(--text-secondary-dark, #aaa);
}
:global(.dark-theme) .statCard,
:global(.dark-theme) .infoSection {
background: var(--surface-dark, #1a1a1a);
border-color: var(--border-dark, #333);
}
:global(.dark-theme) .statValue,
:global(.dark-theme) .infoSection h3,
:global(.dark-theme) .infoValue {
color: var(--text-primary-dark, #ffffff);
}
:global(.dark-theme) .statLabel {
color: var(--text-secondary-dark, #aaa);
}
:global(.dark-theme) .infoLabel {
color: var(--text-tertiary-dark, #888);
}

View file

@ -0,0 +1,11 @@
/**
* Trustee Views Export
*/
export { TrusteeDashboardView } from './TrusteeDashboardView';
export { TrusteeContractsView } from './TrusteeContractsView';
export { TrusteeOrganisationsView } from './TrusteeOrganisationsView';
export { TrusteeDocumentsView } from './TrusteeDocumentsView';
export { TrusteePositionsView } from './TrusteePositionsView';
export { TrusteeRolesView } from './TrusteeRolesView';
export { TrusteeAccessView } from './TrusteeAccessView';

View file

@ -1,6 +1,7 @@
:root { :root {
--color-bg: #F8F9FA; /* war vorher surface */ /* Original color definitions */
--color-surface: #EFEDE5; /* war vorher bg */ --color-bg: #F8F9FA;
--color-surface: #EFEDE5;
--color-text: #3A3A3A; --color-text: #3A3A3A;
--color-primary: #C7C5B2; --color-primary: #C7C5B2;
@ -37,10 +38,50 @@
--object-radius-small: 5px; --object-radius-small: 5px;
} }
/* Dark theme overrides */ /* ============================================== */
/* LIGHT THEME */
/* ============================================== */
:root, .light-theme {
/* Background colors */
--bg-primary: #ffffff;
--bg-secondary: #F8F9FA;
--bg-dark: #f5f5f5;
/* Surface colors */
--surface-color: #f8f9fa;
--surface-dark: #f0f0f0;
/* Text colors */
--text-primary: #1a1a1a;
--text-secondary: #666666;
--text-tertiary: #888888;
--text-primary-dark: #1a1a1a;
--text-secondary-dark: #666666;
--text-tertiary-dark: #888888;
/* Border colors */
--border-color: #e0e0e0;
--border-dark: #d0d0d0;
/* Primary accent color */
--primary-color: #F25843;
--primary-light: rgba(242, 88, 67, 0.12);
--primary-dark-bg: rgba(242, 88, 67, 0.08);
/* Hover backgrounds */
--hover-bg: rgba(0, 0, 0, 0.04);
--hover-bg-dark: rgba(0, 0, 0, 0.06);
/* Error color */
--error-color: #dc2626;
}
/* ============================================== */
/* DARK THEME */
/* ============================================== */
.dark-theme { .dark-theme {
--color-bg: #181818; /* war vorher surface */ --color-bg: #181818;
--color-surface: #1E1D1A; /* war vorher bg */ --color-surface: #1E1D1A;
--color-text: #E5E7EB; --color-text: #E5E7EB;
--color-primary: #C7C5B2; --color-primary: #C7C5B2;
@ -62,5 +103,37 @@
--color-gray: #181818; --color-gray: #181818;
--color-gray-hover: #2E2E2E; --color-gray-hover: #2E2E2E;
--color-gray-disabled: #505050; --color-gray-disabled: #505050;
}
/* Background colors */
--bg-primary: #181818;
--bg-secondary: #1E1D1A;
--bg-dark: #0a0a0a;
/* Surface colors */
--surface-color: #1E1D1A;
--surface-dark: #1a1a1a;
/* Text colors */
--text-primary: #E5E7EB;
--text-secondary: #C7C5B2;
--text-tertiary: #9CA3AF;
--text-primary-dark: #E5E7EB;
--text-secondary-dark: #C7C5B2;
--text-tertiary-dark: #9CA3AF;
/* Border colors */
--border-color: rgba(199, 197, 178, 0.15);
--border-dark: rgba(199, 197, 178, 0.15);
/* Primary accent color */
--primary-color: #F25843;
--primary-light: #FF9A8A; /* Lighter red for text on dark backgrounds */
--primary-dark-bg: rgba(242, 88, 67, 0.15); /* Semi-transparent red for backgrounds */
/* Hover backgrounds */
--hover-bg: rgba(255, 255, 255, 0.06);
--hover-bg-dark: rgba(255, 255, 255, 0.06);
/* Error color */
--error-color: #ef4444;
}

View file

@ -1,6 +1,33 @@
import { defineConfig, loadEnv } from 'vite'; import { defineConfig, loadEnv, Plugin } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import { createHtmlPlugin } from 'vite-plugin-html'; import { createHtmlPlugin } from 'vite-plugin-html';
import * as fs from 'fs';
import * as path from 'path';
// Custom plugin to serve static HTML files from public directory BEFORE SPA fallback
function serveStaticHtml(): Plugin {
return {
name: 'serve-static-html',
enforce: 'pre',
configureServer(server) {
// Directly adding middleware runs BEFORE internal Vite middlewares
server.middlewares.use((req, res, next) => {
// Check if request is for a static HTML file in public
const url = req.url?.split('?')[0]; // Remove query string
if (url && url.endsWith('.html') && url !== '/' && url !== '/index.html') {
const filePath = path.join(process.cwd(), 'public', url);
if (fs.existsSync(filePath)) {
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.statusCode = 200;
res.end(fs.readFileSync(filePath, 'utf-8'));
return;
}
}
next();
});
},
};
}
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
// Load env file based on mode // Load env file based on mode
@ -8,13 +35,23 @@ export default defineConfig(({ mode }) => {
return { return {
plugins: [ plugins: [
// Serve static HTML files
serveStaticHtml(),
react(), react(),
createHtmlPlugin({ createHtmlPlugin({
inject: { // Only process main index.html, not public static files
pages: [
{
entry: 'src/main.tsx',
filename: 'index.html',
template: 'index.html',
injectOptions: {
data: { data: {
VITE_APP_NAME: env.VITE_APP_NAME || 'PowerOn', VITE_APP_NAME: env.VITE_APP_NAME || 'PowerOn',
}, },
}, },
},
],
}), }),
], ],
envPrefix: 'VITE_', envPrefix: 'VITE_',
@ -23,5 +60,7 @@ export default defineConfig(({ mode }) => {
scopeBehaviour: 'local', // Default behavior for CSS modules scopeBehaviour: 'local', // Default behavior for CSS modules
}, },
}, },
// Ensure public files are served correctly as static
publicDir: 'public',
}; };
}); });