revised ui components
|
|
@ -2,9 +2,13 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<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" />
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 71 KiB |
BIN
public/logos/poweron-logo.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
|
|
@ -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
|
|
@ -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>© 2025 PowerOn AI Platform. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
290
public/poweron-privacy.html
Normal 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>© 2025 PowerOn AI Platform. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
333
public/poweron-terms.html
Normal 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>© 2025 PowerOn AI Platform. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -35,6 +35,7 @@ import { FeatureLayout } from './layouts/FeatureLayout';
|
|||
import { DashboardPage } from './pages/Dashboard';
|
||||
import { SettingsPage } from './pages/Settings';
|
||||
import { FeatureViewPage } from './pages/FeatureView';
|
||||
import { AdminMandatesPage, AdminUsersPage, AdminRolesPage } from './pages/admin';
|
||||
|
||||
function App() {
|
||||
// Load saved theme preference and set app name on app mount
|
||||
|
|
@ -115,9 +116,9 @@ function App() {
|
|||
{/* ADMIN ROUTES (nur SysAdmin) */}
|
||||
{/* ============================================== */}
|
||||
<Route path="admin">
|
||||
<Route path="mandates" element={<div>Admin: Mandanten (TODO)</div>} />
|
||||
<Route path="users" element={<div>Admin: Benutzer (TODO)</div>} />
|
||||
<Route path="roles" element={<div>Admin: Globale Rollen (TODO)</div>} />
|
||||
<Route path="mandates" element={<AdminMandatesPage />} />
|
||||
<Route path="users" element={<AdminUsersPage />} />
|
||||
<Route path="roles" element={<AdminRolesPage />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
|
|
|||
|
|
@ -85,15 +85,18 @@ export interface UsernameAvailabilityResponse {
|
|||
message: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
// User-Typ wird aus userApi.ts importiert
|
||||
// Hier nur für Rückwärtskompatibilität
|
||||
export interface AuthUser {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
language: string;
|
||||
enabled: boolean;
|
||||
privilege: string;
|
||||
mandateId: string;
|
||||
roleLabels?: string[];
|
||||
authenticationAuthority: string;
|
||||
isSysAdmin?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +141,7 @@ export async function loginApi(loginData: LoginRequest): Promise<LoginResponse>
|
|||
* Fetch current user data
|
||||
* 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';
|
||||
|
||||
if (authAuthority === 'msft') {
|
||||
|
|
@ -147,7 +150,7 @@ export async function fetchCurrentUserApi(authAuthority?: string): Promise<User>
|
|||
endpoint = '/api/google/me';
|
||||
}
|
||||
|
||||
const response = await api.get<User>(endpoint);
|
||||
const response = await api.get<AuthUser>(endpoint);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ const MOCK_RESPONSE: FeaturesMyResponse = {
|
|||
};
|
||||
|
||||
// Flag für Mock-Modus (auf false setzen wenn Backend bereit)
|
||||
const USE_MOCK = true;
|
||||
const USE_MOCK = false;
|
||||
|
||||
// =============================================================================
|
||||
// API FUNCTIONS
|
||||
|
|
|
|||
|
|
@ -62,15 +62,16 @@ export interface PaginatedResponse<T> {
|
|||
}
|
||||
|
||||
export interface CreatePromptData {
|
||||
mandateId: string;
|
||||
name: string;
|
||||
content: string;
|
||||
// mandateId wird nicht mehr vom Client gesendet
|
||||
// Das Backend bestimmt den Kontext über die instanceId
|
||||
}
|
||||
|
||||
export interface UpdatePromptData {
|
||||
mandateId: string;
|
||||
name: string;
|
||||
content: string;
|
||||
// mandateId wird nicht mehr vom Client gesendet
|
||||
}
|
||||
|
||||
// Type for the request function passed to API functions
|
||||
|
|
|
|||
|
|
@ -1,3 +1,12 @@
|
|||
/**
|
||||
* Trustee API
|
||||
*
|
||||
* API-Funktionen für das Trustee-Feature.
|
||||
* Alle Endpunkte erfordern eine instanceId für den Feature-Instanz-Kontext.
|
||||
*
|
||||
* URL-Struktur: /api/trustee/{instanceId}/{entity}
|
||||
*/
|
||||
|
||||
import { ApiRequestOptions } from '../hooks/useApi';
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -60,7 +69,7 @@ export interface TrusteeDocument {
|
|||
contractId: string;
|
||||
documentName: string;
|
||||
documentMimeType: string;
|
||||
documentData?: any; // Binary data, typically not included in list responses
|
||||
documentData?: any;
|
||||
mandateId?: string;
|
||||
_createdAt?: number;
|
||||
_modifiedAt?: number;
|
||||
|
|
@ -150,16 +159,24 @@ function _buildPaginationParams(params?: PaginationParams): Record<string, any>
|
|||
return requestParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt die Basis-URL für Trustee-Endpunkte
|
||||
*/
|
||||
function _getTrusteeBaseUrl(instanceId: string): string {
|
||||
return `/api/trustee/${instanceId}`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ORGANISATION API
|
||||
// ============================================================================
|
||||
|
||||
export async function fetchOrganisations(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
params?: PaginationParams
|
||||
): Promise<PaginatedResponse<TrusteeOrganisation> | TrusteeOrganisation[]> {
|
||||
return await request({
|
||||
url: '/api/trustee/organisations',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/organisations`,
|
||||
method: 'get',
|
||||
params: _buildPaginationParams(params)
|
||||
});
|
||||
|
|
@ -167,11 +184,12 @@ export async function fetchOrganisations(
|
|||
|
||||
export async function fetchOrganisationById(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
orgId: string
|
||||
): Promise<TrusteeOrganisation | null> {
|
||||
try {
|
||||
return await request({
|
||||
url: `/api/trustee/organisations/${orgId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`,
|
||||
method: 'get'
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
|
@ -182,10 +200,11 @@ export async function fetchOrganisationById(
|
|||
|
||||
export async function createOrganisation(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
data: Partial<TrusteeOrganisation>
|
||||
): Promise<TrusteeOrganisation> {
|
||||
return await request({
|
||||
url: '/api/trustee/organisations',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/organisations`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
|
|
@ -193,11 +212,12 @@ export async function createOrganisation(
|
|||
|
||||
export async function updateOrganisation(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
orgId: string,
|
||||
data: Partial<TrusteeOrganisation>
|
||||
): Promise<TrusteeOrganisation> {
|
||||
return await request({
|
||||
url: `/api/trustee/organisations/${orgId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`,
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
|
|
@ -205,10 +225,11 @@ export async function updateOrganisation(
|
|||
|
||||
export async function deleteOrganisation(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
orgId: string
|
||||
): Promise<void> {
|
||||
await request({
|
||||
url: `/api/trustee/organisations/${orgId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
|
@ -219,10 +240,11 @@ export async function deleteOrganisation(
|
|||
|
||||
export async function fetchRoles(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
params?: PaginationParams
|
||||
): Promise<PaginatedResponse<TrusteeRole> | TrusteeRole[]> {
|
||||
return await request({
|
||||
url: '/api/trustee/roles',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/roles`,
|
||||
method: 'get',
|
||||
params: _buildPaginationParams(params)
|
||||
});
|
||||
|
|
@ -230,11 +252,12 @@ export async function fetchRoles(
|
|||
|
||||
export async function fetchRoleById(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
roleId: string
|
||||
): Promise<TrusteeRole | null> {
|
||||
try {
|
||||
return await request({
|
||||
url: `/api/trustee/roles/${roleId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`,
|
||||
method: 'get'
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
|
@ -245,10 +268,11 @@ export async function fetchRoleById(
|
|||
|
||||
export async function createRole(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
data: Partial<TrusteeRole>
|
||||
): Promise<TrusteeRole> {
|
||||
return await request({
|
||||
url: '/api/trustee/roles',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/roles`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
|
|
@ -256,11 +280,12 @@ export async function createRole(
|
|||
|
||||
export async function updateRole(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
roleId: string,
|
||||
data: Partial<TrusteeRole>
|
||||
): Promise<TrusteeRole> {
|
||||
return await request({
|
||||
url: `/api/trustee/roles/${roleId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`,
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
|
|
@ -268,10 +293,11 @@ export async function updateRole(
|
|||
|
||||
export async function deleteRole(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
roleId: string
|
||||
): Promise<void> {
|
||||
await request({
|
||||
url: `/api/trustee/roles/${roleId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
|
@ -282,10 +308,11 @@ export async function deleteRole(
|
|||
|
||||
export async function fetchAccess(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
params?: PaginationParams
|
||||
): Promise<PaginatedResponse<TrusteeAccess> | TrusteeAccess[]> {
|
||||
return await request({
|
||||
url: '/api/trustee/access',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/access`,
|
||||
method: 'get',
|
||||
params: _buildPaginationParams(params)
|
||||
});
|
||||
|
|
@ -293,11 +320,12 @@ export async function fetchAccess(
|
|||
|
||||
export async function fetchAccessById(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
accessId: string
|
||||
): Promise<TrusteeAccess | null> {
|
||||
try {
|
||||
return await request({
|
||||
url: `/api/trustee/access/${accessId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`,
|
||||
method: 'get'
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
|
@ -308,30 +336,33 @@ export async function fetchAccessById(
|
|||
|
||||
export async function fetchAccessByOrganisation(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
orgId: string
|
||||
): Promise<TrusteeAccess[]> {
|
||||
return await request({
|
||||
url: `/api/trustee/access/organisation/${orgId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/access/organisation/${orgId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchAccessByUser(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
userId: string
|
||||
): Promise<TrusteeAccess[]> {
|
||||
return await request({
|
||||
url: `/api/trustee/access/user/${userId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/access/user/${userId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export async function createAccess(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
data: Partial<TrusteeAccess>
|
||||
): Promise<TrusteeAccess> {
|
||||
return await request({
|
||||
url: '/api/trustee/access',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/access`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
|
|
@ -339,11 +370,12 @@ export async function createAccess(
|
|||
|
||||
export async function updateAccess(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
accessId: string,
|
||||
data: Partial<TrusteeAccess>
|
||||
): Promise<TrusteeAccess> {
|
||||
return await request({
|
||||
url: `/api/trustee/access/${accessId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`,
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
|
|
@ -351,10 +383,11 @@ export async function updateAccess(
|
|||
|
||||
export async function deleteAccess(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
accessId: string
|
||||
): Promise<void> {
|
||||
await request({
|
||||
url: `/api/trustee/access/${accessId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
|
@ -365,10 +398,11 @@ export async function deleteAccess(
|
|||
|
||||
export async function fetchContracts(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
params?: PaginationParams
|
||||
): Promise<PaginatedResponse<TrusteeContract> | TrusteeContract[]> {
|
||||
return await request({
|
||||
url: '/api/trustee/contracts',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/contracts`,
|
||||
method: 'get',
|
||||
params: _buildPaginationParams(params)
|
||||
});
|
||||
|
|
@ -376,11 +410,12 @@ export async function fetchContracts(
|
|||
|
||||
export async function fetchContractById(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
contractId: string
|
||||
): Promise<TrusteeContract | null> {
|
||||
try {
|
||||
return await request({
|
||||
url: `/api/trustee/contracts/${contractId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`,
|
||||
method: 'get'
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
|
@ -391,20 +426,22 @@ export async function fetchContractById(
|
|||
|
||||
export async function fetchContractsByOrganisation(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
orgId: string
|
||||
): Promise<TrusteeContract[]> {
|
||||
return await request({
|
||||
url: `/api/trustee/contracts/organisation/${orgId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/contracts/organisation/${orgId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export async function createContract(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
data: Partial<TrusteeContract>
|
||||
): Promise<TrusteeContract> {
|
||||
return await request({
|
||||
url: '/api/trustee/contracts',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/contracts`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
|
|
@ -412,11 +449,12 @@ export async function createContract(
|
|||
|
||||
export async function updateContract(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
contractId: string,
|
||||
data: Partial<TrusteeContract>
|
||||
): Promise<TrusteeContract> {
|
||||
return await request({
|
||||
url: `/api/trustee/contracts/${contractId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`,
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
|
|
@ -424,10 +462,11 @@ export async function updateContract(
|
|||
|
||||
export async function deleteContract(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
contractId: string
|
||||
): Promise<void> {
|
||||
await request({
|
||||
url: `/api/trustee/contracts/${contractId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
|
@ -438,10 +477,11 @@ export async function deleteContract(
|
|||
|
||||
export async function fetchDocuments(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
params?: PaginationParams
|
||||
): Promise<PaginatedResponse<TrusteeDocument> | TrusteeDocument[]> {
|
||||
return await request({
|
||||
url: '/api/trustee/documents',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/documents`,
|
||||
method: 'get',
|
||||
params: _buildPaginationParams(params)
|
||||
});
|
||||
|
|
@ -449,11 +489,12 @@ export async function fetchDocuments(
|
|||
|
||||
export async function fetchDocumentById(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
documentId: string
|
||||
): Promise<TrusteeDocument | null> {
|
||||
try {
|
||||
return await request({
|
||||
url: `/api/trustee/documents/${documentId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`,
|
||||
method: 'get'
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
|
@ -464,20 +505,22 @@ export async function fetchDocumentById(
|
|||
|
||||
export async function fetchDocumentsByContract(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
contractId: string
|
||||
): Promise<TrusteeDocument[]> {
|
||||
return await request({
|
||||
url: `/api/trustee/documents/contract/${contractId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/documents/contract/${contractId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export async function createDocument(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
data: Partial<TrusteeDocument>
|
||||
): Promise<TrusteeDocument> {
|
||||
return await request({
|
||||
url: '/api/trustee/documents',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/documents`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
|
|
@ -485,11 +528,12 @@ export async function createDocument(
|
|||
|
||||
export async function updateDocument(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
documentId: string,
|
||||
data: Partial<TrusteeDocument>
|
||||
): Promise<TrusteeDocument> {
|
||||
return await request({
|
||||
url: `/api/trustee/documents/${documentId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`,
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
|
|
@ -497,10 +541,11 @@ export async function updateDocument(
|
|||
|
||||
export async function deleteDocument(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
documentId: string
|
||||
): Promise<void> {
|
||||
await request({
|
||||
url: `/api/trustee/documents/${documentId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
|
@ -511,10 +556,11 @@ export async function deleteDocument(
|
|||
|
||||
export async function fetchPositions(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
params?: PaginationParams
|
||||
): Promise<PaginatedResponse<TrusteePosition> | TrusteePosition[]> {
|
||||
return await request({
|
||||
url: '/api/trustee/positions',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/positions`,
|
||||
method: 'get',
|
||||
params: _buildPaginationParams(params)
|
||||
});
|
||||
|
|
@ -522,11 +568,12 @@ export async function fetchPositions(
|
|||
|
||||
export async function fetchPositionById(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
positionId: string
|
||||
): Promise<TrusteePosition | null> {
|
||||
try {
|
||||
return await request({
|
||||
url: `/api/trustee/positions/${positionId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`,
|
||||
method: 'get'
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
|
@ -537,30 +584,33 @@ export async function fetchPositionById(
|
|||
|
||||
export async function fetchPositionsByContract(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
contractId: string
|
||||
): Promise<TrusteePosition[]> {
|
||||
return await request({
|
||||
url: `/api/trustee/positions/contract/${contractId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/positions/contract/${contractId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchPositionsByOrganisation(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
orgId: string
|
||||
): Promise<TrusteePosition[]> {
|
||||
return await request({
|
||||
url: `/api/trustee/positions/organisation/${orgId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/positions/organisation/${orgId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export async function createPosition(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
data: Partial<TrusteePosition>
|
||||
): Promise<TrusteePosition> {
|
||||
return await request({
|
||||
url: '/api/trustee/positions',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/positions`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
|
|
@ -568,11 +618,12 @@ export async function createPosition(
|
|||
|
||||
export async function updatePosition(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
positionId: string,
|
||||
data: Partial<TrusteePosition>
|
||||
): Promise<TrusteePosition> {
|
||||
return await request({
|
||||
url: `/api/trustee/positions/${positionId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`,
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
|
|
@ -580,10 +631,11 @@ export async function updatePosition(
|
|||
|
||||
export async function deletePosition(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
positionId: string
|
||||
): Promise<void> {
|
||||
await request({
|
||||
url: `/api/trustee/positions/${positionId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
|
@ -594,10 +646,11 @@ export async function deletePosition(
|
|||
|
||||
export async function fetchPositionDocuments(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
params?: PaginationParams
|
||||
): Promise<PaginatedResponse<TrusteePositionDocument> | TrusteePositionDocument[]> {
|
||||
return await request({
|
||||
url: '/api/trustee/position-documents',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`,
|
||||
method: 'get',
|
||||
params: _buildPaginationParams(params)
|
||||
});
|
||||
|
|
@ -605,11 +658,12 @@ export async function fetchPositionDocuments(
|
|||
|
||||
export async function fetchPositionDocumentById(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
linkId: string
|
||||
): Promise<TrusteePositionDocument | null> {
|
||||
try {
|
||||
return await request({
|
||||
url: `/api/trustee/position-documents/${linkId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`,
|
||||
method: 'get'
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
|
@ -620,30 +674,33 @@ export async function fetchPositionDocumentById(
|
|||
|
||||
export async function fetchDocumentsForPosition(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
positionId: string
|
||||
): Promise<TrusteePositionDocument[]> {
|
||||
return await request({
|
||||
url: `/api/trustee/position-documents/position/${positionId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/position/${positionId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchPositionsForDocument(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
documentId: string
|
||||
): Promise<TrusteePositionDocument[]> {
|
||||
return await request({
|
||||
url: `/api/trustee/position-documents/document/${documentId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/document/${documentId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export async function createPositionDocument(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
data: Partial<TrusteePositionDocument>
|
||||
): Promise<TrusteePositionDocument> {
|
||||
return await request({
|
||||
url: '/api/trustee/position-documents',
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
|
|
@ -651,10 +708,11 @@ export async function createPositionDocument(
|
|||
|
||||
export async function deletePositionDocument(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
linkId: string
|
||||
): Promise<void> {
|
||||
await request({
|
||||
url: `/api/trustee/position-documents/${linkId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,10 @@ export interface User {
|
|||
enabled: boolean;
|
||||
roleLabels?: string[]; // Array of role labels from backend (e.g., ["user"])
|
||||
authenticationAuthority: string;
|
||||
mandateId: string;
|
||||
[key: string]: any; // Allow additional properties (may include deprecated 'privilege' from backend)
|
||||
isSysAdmin?: boolean; // System-Administrator Flag
|
||||
// mandateId ist nicht mehr Teil des User-Objekts (Multi-Tenant-Konzept)
|
||||
// Der Mandant-Kontext wird über Feature-Instanzen bestimmt
|
||||
[key: string]: any; // Allow additional properties
|
||||
}
|
||||
|
||||
export type UserUpdateData = Partial<Omit<User, 'id' | 'mandateId'>>;
|
||||
|
|
|
|||
|
|
@ -84,15 +84,23 @@ export function EditActionButton<T = any>({
|
|||
const handleClick = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!isDisabled && !loading && !isEditing && !internalLoading && !fetchingData && !isPopupOpen) {
|
||||
// If onEdit callback is provided, call it and return early (custom handling)
|
||||
// The page will handle opening its own modal/form
|
||||
if (onEdit) {
|
||||
setInternalLoading(true);
|
||||
try {
|
||||
await onEdit(row);
|
||||
} finally {
|
||||
setInternalLoading(false);
|
||||
}
|
||||
return; // Don't open the built-in popup when custom onEdit is provided
|
||||
}
|
||||
|
||||
// Otherwise, use the built-in popup form
|
||||
setInternalLoading(true);
|
||||
setFetchingData(true);
|
||||
|
||||
try {
|
||||
// Call the onEdit callback if provided
|
||||
if (onEdit) {
|
||||
await onEdit(row);
|
||||
}
|
||||
|
||||
const itemId = (row as any)[idField];
|
||||
|
||||
// Fetch current item data - use generic fetch function from hookData
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
* MandateNavigation
|
||||
*
|
||||
* Hierarchische Navigation für das Multi-Tenant-System.
|
||||
* Verwendet TreeNavigation für flexible Baumstruktur.
|
||||
*
|
||||
* Struktur:
|
||||
* - SYSTEM (immer verfügbar)
|
||||
|
|
@ -16,13 +17,13 @@
|
|||
* - ADMINISTRATION (nur für SysAdmin)
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useMandates, useFeatureStore } from '../../stores/featureStore';
|
||||
import { useCurrentUser } from '../../hooks/useUsers';
|
||||
import { FEATURE_REGISTRY, getLabel } from '../../types/mandate';
|
||||
import type { Mandate, MandateFeature, FeatureInstance } from '../../types/mandate';
|
||||
import { FaHome, FaCog, FaChevronDown, FaChevronRight, FaBriefcase, FaRobot, FaPlay } from 'react-icons/fa';
|
||||
import { RiAdminFill } from 'react-icons/ri';
|
||||
import { FaHome, FaCog, FaBriefcase, FaRobot, FaPlay, FaBuilding, FaUsers, FaUserShield } from 'react-icons/fa';
|
||||
import { TreeNavigation, type TreeItem, type TreeNodeItem } from './TreeNavigation';
|
||||
import styles from './MandateNavigation.module.css';
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -36,263 +37,93 @@ const FEATURE_ICONS: Record<string, React.ReactNode> = {
|
|||
};
|
||||
|
||||
// =============================================================================
|
||||
// SYSTEM SECTION
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
const SystemSection: React.FC = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<span className={styles.sectionTitle}>SYSTEM</span>
|
||||
</div>
|
||||
<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
|
||||
/**
|
||||
* Convert a FeatureInstance to TreeNodeItem
|
||||
*/
|
||||
function instanceToTreeNode(
|
||||
instance: FeatureInstance,
|
||||
mandateId: string,
|
||||
featureCode: string
|
||||
): TreeNodeItem {
|
||||
const basePath = `/mandates/${mandateId}/${featureCode}/${instance.id}`;
|
||||
const isInInstance = location.pathname.startsWith(basePath);
|
||||
|
||||
// Auto-expand wenn wir in der Instanz sind
|
||||
React.useEffect(() => {
|
||||
if (isInInstance && !isExpanded) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [isInInstance]);
|
||||
|
||||
// Views aus Registry holen
|
||||
// Get views from registry
|
||||
const featureConfig = FEATURE_REGISTRY[featureCode];
|
||||
const views = featureConfig?.views || [];
|
||||
|
||||
// Nur Views anzeigen für die der User Berechtigung hat
|
||||
// Filter views based on permissions
|
||||
const visibleViews = views.filter(view => {
|
||||
const viewCode = `${featureCode}-${view.code}`;
|
||||
return instance.permissions?.views?.[viewCode] !== false;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`${styles.instanceGroup} ${isInInstance ? styles.activeInstance : ''}`}>
|
||||
<button
|
||||
className={styles.instanceHeader}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{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 && (
|
||||
<div className={styles.instanceViews}>
|
||||
{visibleViews.map(view => (
|
||||
<NavLink
|
||||
key={view.code}
|
||||
to={`${basePath}/${view.path}`}
|
||||
className={({ isActive }) =>
|
||||
`${styles.viewItem} ${isActive ? styles.active : ''}`
|
||||
}
|
||||
>
|
||||
<span>{getLabel(view.label)}</span>
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// FEATURE NAV GROUP
|
||||
// =============================================================================
|
||||
|
||||
interface FeatureNavGroupProps {
|
||||
feature: MandateFeature;
|
||||
mandateId: string;
|
||||
// Convert views to children
|
||||
const children: TreeNodeItem[] = visibleViews.map(view => ({
|
||||
id: `${instance.id}-${view.code}`,
|
||||
label: getLabel(view.label),
|
||||
path: `${basePath}/${view.path}`,
|
||||
}));
|
||||
|
||||
return {
|
||||
id: instance.id,
|
||||
label: instance.instanceLabel,
|
||||
badge: instance.userRole,
|
||||
children,
|
||||
defaultExpanded: false,
|
||||
};
|
||||
}
|
||||
|
||||
const FeatureNavGroup: React.FC<FeatureNavGroupProps> = ({ feature, mandateId }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
// Prüfe ob wir in diesem Feature sind
|
||||
const featurePath = `/mandates/${mandateId}/${feature.code}`;
|
||||
const isInFeature = location.pathname.startsWith(featurePath);
|
||||
|
||||
// Auto-expand wenn wir im Feature sind
|
||||
React.useEffect(() => {
|
||||
if (isInFeature && !isExpanded) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [isInFeature]);
|
||||
|
||||
/**
|
||||
* Convert a MandateFeature to TreeNodeItem
|
||||
*/
|
||||
function featureToTreeNode(
|
||||
feature: MandateFeature,
|
||||
mandateId: string
|
||||
): TreeNodeItem | null {
|
||||
if (feature.instances.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.featureGroup} ${isInFeature ? styles.activeFeature : ''}`}>
|
||||
<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>
|
||||
const children = feature.instances.map(instance =>
|
||||
instanceToTreeNode(instance, mandateId, feature.code)
|
||||
);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// MANDATE NAV GROUP
|
||||
// =============================================================================
|
||||
|
||||
interface MandateNavGroupProps {
|
||||
mandate: Mandate;
|
||||
|
||||
return {
|
||||
id: `${mandateId}-${feature.code}`,
|
||||
label: getLabel(feature.label),
|
||||
icon: FEATURE_ICONS[feature.code] || <FaBriefcase />,
|
||||
badge: feature.instances.length,
|
||||
children,
|
||||
defaultExpanded: false,
|
||||
};
|
||||
}
|
||||
|
||||
const MandateNavGroup: React.FC<MandateNavGroupProps> = ({ mandate }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const location = useLocation();
|
||||
|
||||
// Prüfe ob wir in diesem Mandanten sind
|
||||
const mandatePath = `/mandates/${mandate.id}`;
|
||||
const isInMandate = location.pathname.startsWith(mandatePath);
|
||||
|
||||
/**
|
||||
* Convert a Mandate to TreeNodeItem
|
||||
*/
|
||||
function mandateToTreeNode(mandate: Mandate): TreeNodeItem | null {
|
||||
if (mandate.features.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.mandateGroup} ${isInMandate ? styles.activeMandate : ''}`}>
|
||||
<button
|
||||
className={styles.mandateHeader}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? <FaChevronDown className={styles.chevron} /> : <FaChevronRight className={styles.chevron} />}
|
||||
<span className={styles.mandateLabel}>{mandate.name}</span>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<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) {
|
||||
const children = mandate.features
|
||||
.map(feature => featureToTreeNode(feature, mandate.id))
|
||||
.filter((node): node is TreeNodeItem => node !== null);
|
||||
|
||||
if (children.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<span className={styles.sectionTitle}>ADMINISTRATION</span>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
return {
|
||||
id: mandate.id,
|
||||
label: mandate.name,
|
||||
children,
|
||||
defaultExpanded: true,
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EMPTY STATE
|
||||
|
|
@ -314,32 +145,97 @@ const EmptyState: React.FC = () => (
|
|||
export const MandateNavigation: React.FC = () => {
|
||||
const mandates = useMandates();
|
||||
const { hasAnyInstance } = useFeatureStore();
|
||||
const { user } = useCurrentUser();
|
||||
|
||||
// TODO: Aus Auth-Store holen
|
||||
const isSysAdmin = false;
|
||||
// Get isSysAdmin from user data
|
||||
const isSysAdmin = user?.isSysAdmin ?? false;
|
||||
|
||||
// Build navigation items using TreeNavigation structure
|
||||
const navigationItems: TreeItem[] = useMemo(() => {
|
||||
const items: TreeItem[] = [];
|
||||
|
||||
// System section (always visible)
|
||||
items.push({
|
||||
type: 'section',
|
||||
title: 'SYSTEM',
|
||||
children: [
|
||||
{
|
||||
id: 'home',
|
||||
label: 'Übersicht',
|
||||
icon: <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 (
|
||||
<div className={styles.navigation}>
|
||||
{/* System-Bereich (immer sichtbar) */}
|
||||
<SystemSection />
|
||||
|
||||
{/* Separator */}
|
||||
<div className={styles.separator} />
|
||||
|
||||
{/* Mandanten & Features */}
|
||||
{hasAnyInstance() ? (
|
||||
mandates.map(mandate => (
|
||||
<MandateNavGroup key={mandate.id} mandate={mandate} />
|
||||
))
|
||||
{hasAnyInstance() || isSysAdmin ? (
|
||||
<TreeNavigation
|
||||
items={navigationItems}
|
||||
autoExpandActive={true}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState />
|
||||
<>
|
||||
<TreeNavigation
|
||||
items={navigationItems.slice(0, 2)} // System section + separator
|
||||
autoExpandActive={true}
|
||||
/>
|
||||
<EmptyState />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Separator vor Admin */}
|
||||
{isSysAdmin && <div className={styles.separator} />}
|
||||
|
||||
{/* Admin-Bereich (nur für SysAdmin) */}
|
||||
<AdminSection isSysAdmin={isSysAdmin} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
378
src/components/Navigation/TreeNavigation/TreeNavigation.tsx
Normal 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;
|
||||
8
src/components/Navigation/TreeNavigation/index.ts
Normal 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';
|
||||
324
src/components/Navigation/UserSection.module.css
Normal 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);
|
||||
}
|
||||
155
src/components/Navigation/UserSection.tsx
Normal 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;
|
||||
|
|
@ -70,7 +70,7 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ isMinimized = false }) => {
|
|||
enabled: cached.enabled ?? true, // Assume enabled if logged in
|
||||
roleLabels: cached.roleLabels || [],
|
||||
authenticationAuthority: cached.authenticationAuthority || 'local',
|
||||
mandateId: cached.mandateId || ''
|
||||
isSysAdmin: cached.isSysAdmin || false
|
||||
};
|
||||
setUser(userData);
|
||||
setUserError(null);
|
||||
|
|
@ -99,7 +99,7 @@ const SidebarUser: React.FC<SidebarUserProps> = ({ isMinimized = false }) => {
|
|||
enabled: cached.enabled ?? true,
|
||||
roleLabels: cached.roleLabels || [],
|
||||
authenticationAuthority: cached.authenticationAuthority || 'local',
|
||||
mandateId: cached.mandateId || ''
|
||||
isSysAdmin: cached.isSysAdmin || false
|
||||
};
|
||||
setUser(userData);
|
||||
setUserError(null);
|
||||
|
|
|
|||
|
|
@ -15,12 +15,8 @@ import type { FeatureInstance, Mandate, MandateFeature } from '../types/mandate'
|
|||
// URL PARAMETER TYPES
|
||||
// =============================================================================
|
||||
|
||||
export interface FeatureRouteParams {
|
||||
mandateId?: string;
|
||||
featureCode?: string;
|
||||
instanceId?: string;
|
||||
'*'?: string; // Wildcard für Sub-Pfade
|
||||
}
|
||||
// Route-Parameter werden als Record<string, string | undefined> erwartet
|
||||
// Wir verwenden daher einen einfachen Typ-Alias
|
||||
|
||||
// =============================================================================
|
||||
// RETURN TYPES
|
||||
|
|
@ -63,7 +59,7 @@ export interface CurrentInstanceContext {
|
|||
* ```
|
||||
*/
|
||||
export function useCurrentInstance(): CurrentInstanceContext {
|
||||
const params = useParams<FeatureRouteParams>();
|
||||
const params = useParams();
|
||||
const { getMandateById, getFeatureByCode, getInstanceById, loading } = useFeatureStore();
|
||||
|
||||
const mandateId = params.mandateId;
|
||||
|
|
@ -108,7 +104,7 @@ export function useInstance(): FeatureInstance | undefined {
|
|||
* Hook für die Instanz-ID aus der URL
|
||||
*/
|
||||
export function useInstanceId(): string | undefined {
|
||||
const params = useParams<FeatureRouteParams>();
|
||||
const params = useParams();
|
||||
return params.instanceId;
|
||||
}
|
||||
|
||||
|
|
@ -116,7 +112,7 @@ export function useInstanceId(): string | undefined {
|
|||
* Hook für den Feature-Code aus der URL
|
||||
*/
|
||||
export function useFeatureCode(): string | undefined {
|
||||
const params = useParams<FeatureRouteParams>();
|
||||
const params = useParams();
|
||||
return params.featureCode;
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +120,7 @@ export function useFeatureCode(): string | undefined {
|
|||
* Hook für die Mandate-ID aus der URL
|
||||
*/
|
||||
export function useMandateId(): string | undefined {
|
||||
const params = useParams<FeatureRouteParams>();
|
||||
const params = useParams();
|
||||
return params.mandateId;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Die Berechtigungen werden summarisch pro Instanz geladen (kein einzelner API-Call pro Check).
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCurrentInstance } from './useCurrentInstance';
|
||||
import type {
|
||||
TablePermission,
|
||||
|
|
@ -27,10 +27,6 @@ const NO_ACCESS_TABLE: TablePermission = {
|
|||
delete: 'n',
|
||||
};
|
||||
|
||||
const NO_ACCESS_FIELD: FieldPermission = {
|
||||
read: false,
|
||||
write: false,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// TABLE PERMISSION HOOKS
|
||||
235
src/hooks/useMandates.ts
Normal 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;
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useApiRequest } from './useApi';
|
||||
import { getUserDataCache } from '../utils/userCache';
|
||||
import api from '../api';
|
||||
import { usePermissions, type UserPermissions } from './usePermissions';
|
||||
import {
|
||||
|
|
@ -507,13 +506,9 @@ export function usePromptOperations() {
|
|||
setCreatingPrompt(true);
|
||||
|
||||
try {
|
||||
// Get mandateId from currentUser in sessionStorage cache
|
||||
const currentUserData = getUserDataCache();
|
||||
const mandateId = currentUserData?.mandateId || '';
|
||||
|
||||
// Structure the request body as required by the API
|
||||
// mandateId wird nicht mehr vom Client gesendet
|
||||
// Das Backend bestimmt den Kontext über die instanceId im Request
|
||||
const requestBody = {
|
||||
mandateId: mandateId,
|
||||
name: promptData.name,
|
||||
content: promptData.content
|
||||
};
|
||||
|
|
@ -533,13 +528,8 @@ export function usePromptOperations() {
|
|||
setUpdateError(null);
|
||||
|
||||
try {
|
||||
// Get mandateId from currentUser in sessionStorage cache
|
||||
const currentUserData = getUserDataCache();
|
||||
const mandateId = currentUserData?.mandateId || '';
|
||||
|
||||
// Structure the request body as required by the API
|
||||
// mandateId wird nicht mehr vom Client gesendet
|
||||
const requestBody = {
|
||||
mandateId: mandateId,
|
||||
name: updateData.name,
|
||||
content: updateData.content
|
||||
};
|
||||
|
|
|
|||
235
src/hooks/useRoles.ts
Normal 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;
|
||||
|
|
@ -1,7 +1,15 @@
|
|||
/**
|
||||
* Trustee Hooks
|
||||
*
|
||||
* Hooks für das Trustee-Feature mit Instanz-Kontext.
|
||||
* Die instanceId wird automatisch aus der URL gelesen.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useApiRequest } from './useApi';
|
||||
import api from '../api';
|
||||
import { usePermissions, type UserPermissions } from './usePermissions';
|
||||
import { useInstanceId } from './useCurrentInstance';
|
||||
import {
|
||||
// Types
|
||||
type TrusteeOrganisation,
|
||||
|
|
@ -94,15 +102,18 @@ export interface AttributeDefinition {
|
|||
|
||||
interface TrusteeEntityConfig<T> {
|
||||
entityName: string;
|
||||
fetchAll: (request: any, params?: PaginationParams) => Promise<any>;
|
||||
fetchById: (request: any, id: string) => Promise<T | null>;
|
||||
create: (request: any, data: Partial<T>) => Promise<T>;
|
||||
update: (request: any, id: string, data: Partial<T>) => Promise<T>;
|
||||
deleteItem: (request: any, id: string) => Promise<void>;
|
||||
fetchAll: (request: any, instanceId: string, params?: PaginationParams) => Promise<any>;
|
||||
fetchById: (request: any, instanceId: string, id: string) => Promise<T | null>;
|
||||
create: (request: any, instanceId: string, data: Partial<T>) => Promise<T>;
|
||||
update: (request: any, instanceId: string, id: string, data: Partial<T>) => Promise<T>;
|
||||
deleteItem: (request: any, instanceId: string, id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntityConfig<T>) {
|
||||
return function useTrusteeEntity() {
|
||||
// Hole instanceId aus URL-Kontext
|
||||
const instanceId = useInstanceId();
|
||||
|
||||
const [items, setItems] = useState<T[]>([]);
|
||||
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
|
||||
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
|
||||
|
|
@ -116,8 +127,10 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
const { checkPermission } = usePermissions();
|
||||
|
||||
const fetchAttributes = useCallback(async () => {
|
||||
if (!instanceId) return [];
|
||||
|
||||
try {
|
||||
const response = await api.get(`/api/attributes/${config.entityName}`);
|
||||
const response = await api.get(`/api/trustee/${instanceId}/attributes/${config.entityName}`);
|
||||
let attrs: AttributeDefinition[] = [];
|
||||
if (response.data?.attributes && Array.isArray(response.data.attributes)) {
|
||||
attrs = response.data.attributes;
|
||||
|
|
@ -131,7 +144,7 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
setAttributes([]);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
}, [instanceId]);
|
||||
|
||||
const fetchPermissions = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -153,8 +166,13 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
}, [checkPermission]);
|
||||
|
||||
const fetchItems = useCallback(async (params?: PaginationParams) => {
|
||||
if (!instanceId) {
|
||||
setItems([]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await config.fetchAll(request, params);
|
||||
const data = await config.fetchAll(request, instanceId, params);
|
||||
|
||||
if (data && typeof data === 'object' && 'items' in data) {
|
||||
const fetchedItems = Array.isArray(data.items) ? data.items : [];
|
||||
|
|
@ -171,7 +189,7 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
setItems([]);
|
||||
setPagination(null);
|
||||
}
|
||||
}, [request]);
|
||||
}, [request, instanceId]);
|
||||
|
||||
const removeOptimistically = (itemId: string) => {
|
||||
setItems(prev => prev.filter(item => item.id !== itemId));
|
||||
|
|
@ -188,8 +206,9 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
};
|
||||
|
||||
const fetchById = useCallback(async (itemId: string): Promise<T | null> => {
|
||||
return await config.fetchById(request, itemId);
|
||||
}, [request]);
|
||||
if (!instanceId) return null;
|
||||
return await config.fetchById(request, instanceId, itemId);
|
||||
}, [request, instanceId]);
|
||||
|
||||
const generateEditFieldsFromAttributes = useCallback(() => {
|
||||
if (!attributes || attributes.length === 0) {
|
||||
|
|
@ -198,11 +217,9 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
|
||||
return attributes
|
||||
.filter(attr => {
|
||||
// For EDIT mode: filter out readonly fields and system fields
|
||||
if (attr.readonly === true || attr.editable === false) {
|
||||
return false;
|
||||
}
|
||||
// Also filter out 'id' for edit mode (id cannot be changed)
|
||||
if (attr.name === 'id') {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -265,7 +282,6 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
});
|
||||
}, [attributes]);
|
||||
|
||||
// Generate fields for CREATE forms - includes all required fields like 'id'
|
||||
const generateCreateFieldsFromAttributes = useCallback(() => {
|
||||
if (!attributes || attributes.length === 0) {
|
||||
return [];
|
||||
|
|
@ -273,8 +289,6 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
|
||||
return attributes
|
||||
.filter(attr => {
|
||||
// For CREATE mode: include all user-editable fields including 'id'
|
||||
// Only filter out system-generated fields
|
||||
const systemFields = ['_createdBy', '_createdAt', '_modifiedBy', '_modifiedAt', 'mandateId'];
|
||||
return !systemFields.includes(attr.name);
|
||||
})
|
||||
|
|
@ -325,7 +339,7 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
key: attr.name,
|
||||
label: attr.label || attr.name,
|
||||
type: fieldType,
|
||||
editable: true, // All fields are editable in create mode
|
||||
editable: true,
|
||||
required: attr.required === true,
|
||||
options,
|
||||
optionsReference,
|
||||
|
|
@ -341,14 +355,14 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
return await fetchAttributes();
|
||||
}, [attributes, fetchAttributes]);
|
||||
|
||||
// Lade Daten wenn instanceId verfügbar
|
||||
useEffect(() => {
|
||||
fetchAttributes();
|
||||
fetchPermissions();
|
||||
}, [fetchAttributes, fetchPermissions]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems();
|
||||
}, [fetchItems]);
|
||||
if (instanceId) {
|
||||
fetchAttributes();
|
||||
fetchPermissions();
|
||||
fetchItems();
|
||||
}
|
||||
}, [instanceId, fetchAttributes, fetchPermissions, fetchItems]);
|
||||
|
||||
return {
|
||||
items,
|
||||
|
|
@ -363,13 +377,17 @@ function _createTrusteeEntityHook<T extends { id: string }>(config: TrusteeEntit
|
|||
fetchById,
|
||||
generateEditFieldsFromAttributes,
|
||||
generateCreateFieldsFromAttributes,
|
||||
ensureAttributesLoaded
|
||||
ensureAttributesLoaded,
|
||||
instanceId // Auch instanceId zurückgeben für Operations-Hook
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeEntityConfig<T>) {
|
||||
return function useTrusteeEntityOperations() {
|
||||
// Hole instanceId aus URL-Kontext
|
||||
const instanceId = useInstanceId();
|
||||
|
||||
const [deletingItems, setDeletingItems] = useState<Set<string>>(new Set());
|
||||
const [creatingItem, setCreatingItem] = useState(false);
|
||||
const { request, isLoading } = useApiRequest();
|
||||
|
|
@ -377,12 +395,17 @@ function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeE
|
|||
const [createError, setCreateError] = 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);
|
||||
setDeletingItems(prev => new Set(prev).add(itemId));
|
||||
|
||||
try {
|
||||
await config.deleteItem(request, itemId);
|
||||
await config.deleteItem(request, instanceId, itemId);
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
|
|
@ -395,20 +418,23 @@ function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeE
|
|||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [request, instanceId]);
|
||||
|
||||
const handleCreate = async (itemData: Partial<T>) => {
|
||||
const handleCreate = useCallback(async (itemData: Partial<T>) => {
|
||||
if (!instanceId) {
|
||||
setCreateError('No instance context');
|
||||
return { success: false, error: 'No instance context' };
|
||||
}
|
||||
|
||||
setCreateError(null);
|
||||
setCreatingItem(true);
|
||||
|
||||
// Debug: Log what data is being sent to the backend
|
||||
console.warn('🔧 handleCreate called with itemData:', itemData);
|
||||
|
||||
try {
|
||||
const newItem = await config.create(request, itemData);
|
||||
const newItem = await config.create(request, instanceId, itemData);
|
||||
return { success: true, data: newItem };
|
||||
} catch (error: any) {
|
||||
// Debug: Log full error details
|
||||
console.error('🔧 handleCreate error:', {
|
||||
message: error.message,
|
||||
response: error.response?.data,
|
||||
|
|
@ -420,13 +446,18 @@ function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeE
|
|||
} finally {
|
||||
setCreatingItem(false);
|
||||
}
|
||||
};
|
||||
}, [request, instanceId]);
|
||||
|
||||
const handleUpdate = async (itemId: string, updateData: Partial<T>) => {
|
||||
const handleUpdate = useCallback(async (itemId: string, updateData: Partial<T>) => {
|
||||
if (!instanceId) {
|
||||
setUpdateError('No instance context');
|
||||
return { success: false, error: 'No instance context' };
|
||||
}
|
||||
|
||||
setUpdateError(null);
|
||||
|
||||
try {
|
||||
const updatedItem = await config.update(request, itemId, updateData);
|
||||
const updatedItem = await config.update(request, instanceId, itemId, updateData);
|
||||
return { success: true, data: updatedItem };
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || error.message || 'Failed to update';
|
||||
|
|
@ -439,7 +470,7 @@ function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeE
|
|||
isValidationError: error.response?.status === 400
|
||||
};
|
||||
}
|
||||
};
|
||||
}, [request, instanceId]);
|
||||
|
||||
return {
|
||||
deletingItems,
|
||||
|
|
@ -450,7 +481,8 @@ function _createTrusteeOperationsHook<T extends { id: string }>(config: TrusteeE
|
|||
handleDelete,
|
||||
handleCreate,
|
||||
handleUpdate,
|
||||
isLoading
|
||||
isLoading,
|
||||
instanceId
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
@ -558,7 +590,7 @@ export const useTrusteePositionOperations = _createTrusteeOperationsHook(positio
|
|||
const positionDocumentConfig: TrusteeEntityConfig<TrusteePositionDocument> = {
|
||||
entityName: 'TrusteePositionDocument',
|
||||
fetchAll: fetchPositionDocumentsApi,
|
||||
fetchById: async () => null, // Not typically needed
|
||||
fetchById: async () => null,
|
||||
create: createPositionDocumentApi,
|
||||
update: async () => { throw new Error('Update not supported for position-document links'); },
|
||||
deleteItem: deletePositionDocumentApi
|
||||
|
|
|
|||
|
|
@ -31,25 +31,14 @@ export function useCurrentUser() {
|
|||
// Check if we already have user data in sessionStorage cache
|
||||
const cachedUser = getUserDataCache();
|
||||
if (cachedUser && cachedUser.username) {
|
||||
// Check if cached user has roleLabels - if empty, refetch from API
|
||||
const hasRoleLabels = Array.isArray(cachedUser.roleLabels) && cachedUser.roleLabels.length > 0;
|
||||
|
||||
if (!hasRoleLabels) {
|
||||
console.warn('⚠️ Cached user data has no roleLabels, refetching from API:', {
|
||||
username: cachedUser.username,
|
||||
roleLabels: cachedUser.roleLabels
|
||||
});
|
||||
// Clear cache and continue to fetch from API
|
||||
clearUserDataCache();
|
||||
} else {
|
||||
// Use cached user data - permissions are checked via RBAC API, not client-side
|
||||
setUser(cachedUser);
|
||||
console.log('✅ Using cached user data from sessionStorage (persists during session):', {
|
||||
username: cachedUser.username,
|
||||
roleLabels: cachedUser.roleLabels
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Use cached user data - permissions are checked via RBAC API, not client-side
|
||||
// Note: roleLabels is deprecated in Multi-Tenant architecture - use isSysAdmin flag instead
|
||||
setUser(cachedUser);
|
||||
console.log('✅ Using cached user data from sessionStorage (persists during session):', {
|
||||
username: cachedUser.username,
|
||||
isSysAdmin: cachedUser.isSysAdmin
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// JWT tokens are now stored in httpOnly cookies, so we fetch user data from API
|
||||
|
|
@ -79,49 +68,28 @@ export function useCurrentUser() {
|
|||
|
||||
const data = await fetchCurrentUserApi(request, authAuthority || undefined);
|
||||
|
||||
// Log full response for debugging
|
||||
// Log response for debugging
|
||||
console.log('📦 User data received from API:', {
|
||||
username: data?.username,
|
||||
roleLabels: data?.roleLabels,
|
||||
hasRoleLabels: !!data?.roleLabels,
|
||||
roleLabelsLength: Array.isArray(data?.roleLabels) ? data.roleLabels.length : 0,
|
||||
roleLabelsContent: Array.isArray(data?.roleLabels) ? data.roleLabels : 'not an array',
|
||||
allKeys: data ? Object.keys(data) : [],
|
||||
fullData: JSON.stringify(data, null, 2)
|
||||
isSysAdmin: data?.isSysAdmin,
|
||||
allKeys: data ? Object.keys(data) : []
|
||||
});
|
||||
|
||||
// Always cache user data - permissions are checked via RBAC API, not client-side
|
||||
// roleLabels are optional metadata for display/logging purposes
|
||||
// Validate user data
|
||||
if (!data || !data.username) {
|
||||
console.error('❌ User data from API is invalid:', {
|
||||
username: data?.username,
|
||||
dataKeys: data ? Object.keys(data) : [],
|
||||
fullResponse: data
|
||||
dataKeys: data ? Object.keys(data) : []
|
||||
});
|
||||
throw new Error('Invalid user data received from API');
|
||||
}
|
||||
|
||||
// Check if API returned roleLabels - if not, log warning but still cache
|
||||
const hasRoleLabels = Array.isArray(data.roleLabels) && data.roleLabels.length > 0;
|
||||
|
||||
if (!hasRoleLabels) {
|
||||
console.warn('⚠️ User data from API has no roleLabels - this may cause RBAC issues:', {
|
||||
username: data.username,
|
||||
roleLabels: data.roleLabels,
|
||||
allKeys: Object.keys(data),
|
||||
fullResponse: JSON.stringify(data, null, 2)
|
||||
});
|
||||
// Still cache it, but log the issue - backend RBAC should handle permissions
|
||||
// However, if backend expects roleLabels, this will cause problems
|
||||
}
|
||||
|
||||
// Cache user data (permissions are checked via RBAC API)
|
||||
// Note: roleLabels is deprecated - use isSysAdmin flag for admin checks
|
||||
setUserDataCache(data);
|
||||
console.log('✅ User data fetched from API and cached in sessionStorage (secure):', {
|
||||
console.log('✅ User data fetched from API and cached:', {
|
||||
username: data.username,
|
||||
roleLabels: data.roleLabels,
|
||||
roleLabelsLength: Array.isArray(data.roleLabels) ? data.roleLabels.length : 0,
|
||||
hasRoleLabels
|
||||
isSysAdmin: data.isSysAdmin
|
||||
});
|
||||
setUser(data);
|
||||
} catch (error: any) {
|
||||
|
|
@ -292,25 +260,12 @@ export function useCurrentUser() {
|
|||
// Try to load user from sessionStorage cache first for faster initial load
|
||||
const cachedUser = getUserDataCache();
|
||||
if (cachedUser && cachedUser.username) {
|
||||
// Check if cached user has roleLabels - if empty, refetch from API
|
||||
const hasRoleLabels = Array.isArray(cachedUser.roleLabels) && cachedUser.roleLabels.length > 0;
|
||||
|
||||
if (!hasRoleLabels) {
|
||||
console.warn('⚠️ Cached user data has no roleLabels, refetching from API:', {
|
||||
username: cachedUser.username,
|
||||
roleLabels: cachedUser.roleLabels
|
||||
});
|
||||
// Clear cache and refetch
|
||||
clearUserDataCache();
|
||||
fetchCurrentUser();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use cached user data - permissions are checked via RBAC API
|
||||
// Note: roleLabels is deprecated in Multi-Tenant architecture - use isSysAdmin flag instead
|
||||
setUser(cachedUser);
|
||||
console.log('✅ Using cached user data from sessionStorage on mount (persists during session):', {
|
||||
console.log('✅ Using cached user data from sessionStorage on mount:', {
|
||||
username: cachedUser.username,
|
||||
roleLabels: cachedUser.roleLabels
|
||||
isSysAdmin: cachedUser.isSysAdmin
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -835,20 +790,14 @@ export function useUserOperations() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleUserCreate = async (userData: Omit<User, 'id' | 'mandateId'>) => {
|
||||
const handleUserCreate = async (userData: Omit<User, 'id'>) => {
|
||||
setCreateError(null);
|
||||
setCreatingUser(true);
|
||||
|
||||
try {
|
||||
const currentUserData = getUserDataCache();
|
||||
const mandateId = currentUserData?.mandateId || '';
|
||||
|
||||
const requestBody = {
|
||||
mandateId: mandateId,
|
||||
...userData
|
||||
};
|
||||
|
||||
const newUser = await createUserApi(request, requestBody);
|
||||
// mandateId wird nicht mehr vom Client gesendet
|
||||
// Das Backend bestimmt den Kontext über die instanceId im Request
|
||||
const newUser = await createUserApi(request, userData);
|
||||
|
||||
return { success: true, userData: newUser };
|
||||
} catch (error: any) {
|
||||
|
|
@ -889,15 +838,8 @@ export function useUserOperations() {
|
|||
setEditingUsers(prev => new Set(prev).add(userId));
|
||||
|
||||
try {
|
||||
const currentUserData = getUserDataCache();
|
||||
const mandateId = currentUserData?.mandateId || '';
|
||||
|
||||
const requestBody = {
|
||||
mandateId: mandateId,
|
||||
...updateData
|
||||
};
|
||||
|
||||
const updatedUser = await updateUserApi(request, userId, requestBody);
|
||||
// mandateId wird nicht mehr vom Client gesendet
|
||||
const updatedUser = await updateUserApi(request, userId, updateData);
|
||||
|
||||
return { success: true, userData: updatedUser };
|
||||
} catch (error: any) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ html, body {
|
|||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-family, "DM Sans", sans-serif);
|
||||
}
|
||||
|
||||
#root {
|
||||
|
|
@ -15,4 +16,5 @@ html, body {
|
|||
width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-family, "DM Sans", sans-serif);
|
||||
}
|
||||
|
|
@ -31,6 +31,12 @@
|
|||
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
||||
}
|
||||
|
||||
.logoImage {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logoText {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
|
|
@ -93,6 +99,10 @@
|
|||
border-bottom-color: var(--border-dark, #333);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .logoImage {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .logoPower {
|
||||
color: var(--text-primary-dark, #ffffff);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import React, { useEffect } from 'react';
|
|||
import { Outlet } from 'react-router-dom';
|
||||
import { FeatureProvider, useFeatureStore } from '../stores/featureStore';
|
||||
import { MandateNavigation } from '../components/Navigation/MandateNavigation';
|
||||
import { UserSection } from '../components/Navigation/UserSection';
|
||||
import styles from './MainLayout.module.css';
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -30,10 +31,11 @@ const MainLayoutInner: React.FC = () => {
|
|||
{/* Sidebar */}
|
||||
<aside className={styles.sidebar}>
|
||||
<div className={styles.logoContainer}>
|
||||
<div className={styles.logoText}>
|
||||
<span className={styles.logoPower}>Power</span>
|
||||
<span className={styles.logoOn}>On</span>
|
||||
</div>
|
||||
<img
|
||||
src="/logos/poweron-logo.png"
|
||||
alt="PowerOn"
|
||||
className={styles.logoImage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<nav className={styles.navigation}>
|
||||
|
|
@ -55,9 +57,7 @@ const MainLayoutInner: React.FC = () => {
|
|||
</nav>
|
||||
|
||||
{/* User-Bereich am unteren Rand */}
|
||||
<div className={styles.userSection}>
|
||||
{/* TODO: User-Info Komponente */}
|
||||
</div>
|
||||
<UserSection />
|
||||
</aside>
|
||||
|
||||
{/* Content */}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* View Header */
|
||||
.viewHeader {
|
||||
flex-shrink: 0;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
||||
background: var(--bg-primary, #ffffff);
|
||||
|
|
@ -22,38 +23,33 @@
|
|||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
|
||||
/* View Content */
|
||||
.viewContent {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Placeholder */
|
||||
/* Placeholder View */
|
||||
.placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
padding: 2rem;
|
||||
background: var(--surface-color, #f8f9fa);
|
||||
border: 2px dashed var(--border-color, #e0e0e0);
|
||||
border-radius: 12px;
|
||||
min-height: 400px;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.placeholder h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
|
||||
.placeholder p {
|
||||
margin: 0.5rem 0 0;
|
||||
margin: 0;
|
||||
color: var(--text-secondary, #666);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
/* Not Found */
|
||||
|
|
@ -63,29 +59,22 @@
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
padding: 2rem;
|
||||
min-height: 400px;
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.notFound h2,
|
||||
.accessDenied h2 {
|
||||
margin: 0;
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
|
||||
.notFound p,
|
||||
.accessDenied p {
|
||||
margin: 0.5rem 0 0;
|
||||
margin: 0;
|
||||
color: var(--text-secondary, #666);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.accessDenied {
|
||||
background: var(--error-light, #fef2f2);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.accessDenied h2 {
|
||||
|
|
@ -98,25 +87,14 @@
|
|||
border-bottom-color: var(--border-dark, #333);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .viewTitle {
|
||||
color: var(--text-primary-dark, #ffffff);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .placeholder {
|
||||
background: var(--surface-dark, #1a1a1a);
|
||||
border-color: var(--border-dark, #444);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .viewTitle,
|
||||
:global(.dark-theme) .placeholder h2,
|
||||
:global(.dark-theme) .notFound h2 {
|
||||
color: var(--text-primary-dark, #ffffff);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .placeholder p,
|
||||
:global(.dark-theme) .notFound p {
|
||||
:global(.dark-theme) .notFound p,
|
||||
:global(.dark-theme) .accessDenied p {
|
||||
color: var(--text-secondary-dark, #aaa);
|
||||
}
|
||||
|
||||
:global(.dark-theme) .accessDenied {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,105 +3,55 @@
|
|||
*
|
||||
* Generische Feature-View-Komponente.
|
||||
* Rendert den entsprechenden Content basierend auf Feature-Code und View.
|
||||
*
|
||||
* Die Komponente ist Feature-agnostisch und delegiert an spezifische View-Komponenten.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useCurrentInstance } from '../hooks/useCurrentInstance';
|
||||
import { useCanViewFeatureView } from '../hooks/useInstancePermissions';
|
||||
import { getLabel, FEATURE_REGISTRY } from '../types/mandate';
|
||||
|
||||
// Trustee Views
|
||||
import { TrusteeContractsView } from './views/trustee/TrusteeContractsView';
|
||||
import { TrusteeOrganisationsView } from './views/trustee/TrusteeOrganisationsView';
|
||||
import { TrusteeDocumentsView } from './views/trustee/TrusteeDocumentsView';
|
||||
import { TrusteePositionsView } from './views/trustee/TrusteePositionsView';
|
||||
import { TrusteeRolesView } from './views/trustee/TrusteeRolesView';
|
||||
import { TrusteeAccessView } from './views/trustee/TrusteeAccessView';
|
||||
import { TrusteeDashboardView } from './views/trustee/TrusteeDashboardView';
|
||||
|
||||
import styles from './FeatureView.module.css';
|
||||
|
||||
// =============================================================================
|
||||
// VIEW COMPONENTS (Placeholders - werden später durch echte ersetzt)
|
||||
// PLACEHOLDER VIEWS (für nicht implementierte Features)
|
||||
// =============================================================================
|
||||
|
||||
// Trustee Views
|
||||
const TrusteeDashboard: React.FC = () => (
|
||||
const PlaceholderView: React.FC<{ title: string; description: string }> = ({ title, description }) => (
|
||||
<div className={styles.placeholder}>
|
||||
<h2>Trustee Dashboard</h2>
|
||||
<p>Übersicht der Treuhand-Aktivitäten</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>
|
||||
<h2>{title}</h2>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Chatworkflow Views
|
||||
const ChatworkflowDashboard: React.FC = () => (
|
||||
<div className={styles.placeholder}>
|
||||
<h2>Workflow Dashboard</h2>
|
||||
<p>Übersicht der Workflows</p>
|
||||
</div>
|
||||
<PlaceholderView title="Workflow Dashboard" description="Übersicht der Workflows" />
|
||||
);
|
||||
|
||||
const ChatworkflowRuns: React.FC = () => (
|
||||
<div className={styles.placeholder}>
|
||||
<h2>Runs</h2>
|
||||
<p>Workflow-Ausführungen</p>
|
||||
</div>
|
||||
<PlaceholderView title="Runs" description="Workflow-Ausführungen" />
|
||||
);
|
||||
|
||||
const ChatworkflowFiles: React.FC = () => (
|
||||
<div className={styles.placeholder}>
|
||||
<h2>Dateien</h2>
|
||||
<p>Workflow-Dateien</p>
|
||||
</div>
|
||||
<PlaceholderView title="Dateien" description="Workflow-Dateien" />
|
||||
);
|
||||
|
||||
// Chatbot Views
|
||||
const ChatbotConversations: React.FC = () => (
|
||||
<div className={styles.placeholder}>
|
||||
<h2>Konversationen</h2>
|
||||
<p>Chat-Konversationen</p>
|
||||
</div>
|
||||
<PlaceholderView title="Konversationen" description="Chat-Konversationen" />
|
||||
);
|
||||
|
||||
const ChatbotSettings: React.FC = () => (
|
||||
<div className={styles.placeholder}>
|
||||
<h2>Chatbot Einstellungen</h2>
|
||||
<p>Konfiguration des Chatbots</p>
|
||||
</div>
|
||||
<PlaceholderView title="Chatbot Einstellungen" description="Konfiguration des Chatbots" />
|
||||
);
|
||||
|
||||
// Generic/Fallback
|
||||
|
|
@ -127,13 +77,13 @@ type ViewComponent = React.FC;
|
|||
|
||||
const VIEW_COMPONENTS: Record<string, Record<string, ViewComponent>> = {
|
||||
trustee: {
|
||||
dashboard: TrusteeDashboard,
|
||||
organisations: TrusteeOrganisations,
|
||||
contracts: TrusteeContracts,
|
||||
documents: TrusteeDocuments,
|
||||
positions: TrusteePositions,
|
||||
roles: TrusteeRoles,
|
||||
access: TrusteeAccess,
|
||||
dashboard: TrusteeDashboardView,
|
||||
organisations: TrusteeOrganisationsView,
|
||||
contracts: TrusteeContractsView,
|
||||
documents: TrusteeDocumentsView,
|
||||
positions: TrusteePositionsView,
|
||||
roles: TrusteeRolesView,
|
||||
access: TrusteeAccessView,
|
||||
},
|
||||
chatworkflow: {
|
||||
dashboard: ChatworkflowDashboard,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import styles from './Settings.module.css';
|
|||
// =============================================================================
|
||||
|
||||
export const SettingsPage: React.FC = () => {
|
||||
const { t, language, setLanguage } = useLanguage();
|
||||
const { currentLanguage, setLanguage } = useLanguage();
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>(
|
||||
() => (localStorage.getItem('theme') as 'light' | 'dark') || 'light'
|
||||
);
|
||||
|
|
@ -79,7 +79,7 @@ export const SettingsPage: React.FC = () => {
|
|||
<div className={styles.settingControl}>
|
||||
<select
|
||||
className={styles.select}
|
||||
value={language}
|
||||
value={currentLanguage}
|
||||
onChange={(e) => setLanguage(e.target.value as 'de' | 'en' | 'fr')}
|
||||
>
|
||||
<option value="de">Deutsch</option>
|
||||
|
|
|
|||
286
src/pages/admin/Admin.module.css
Normal 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);
|
||||
}
|
||||
246
src/pages/admin/AdminMandatesPage.tsx
Normal 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;
|
||||
277
src/pages/admin/AdminRolesPage.tsx
Normal 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;
|
||||
325
src/pages/admin/AdminUsersPage.tsx
Normal 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
|
|
@ -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';
|
||||
97
src/pages/views/trustee/TrusteeAccessView.tsx
Normal 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;
|
||||
99
src/pages/views/trustee/TrusteeContractsView.tsx
Normal 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;
|
||||
73
src/pages/views/trustee/TrusteeDashboardView.tsx
Normal 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;
|
||||
98
src/pages/views/trustee/TrusteeDocumentsView.tsx
Normal 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;
|
||||
97
src/pages/views/trustee/TrusteeOrganisationsView.tsx
Normal 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;
|
||||
103
src/pages/views/trustee/TrusteePositionsView.tsx
Normal 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;
|
||||
93
src/pages/views/trustee/TrusteeRolesView.tsx
Normal 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;
|
||||
312
src/pages/views/trustee/TrusteeViews.module.css
Normal 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);
|
||||
}
|
||||
11
src/pages/views/trustee/index.ts
Normal 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';
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
:root {
|
||||
--color-bg: #F8F9FA; /* war vorher surface */
|
||||
--color-surface: #EFEDE5; /* war vorher bg */
|
||||
/* Original color definitions */
|
||||
--color-bg: #F8F9FA;
|
||||
--color-surface: #EFEDE5;
|
||||
--color-text: #3A3A3A;
|
||||
|
||||
--color-primary: #C7C5B2;
|
||||
|
|
@ -36,11 +37,51 @@
|
|||
--object-radius-medium: 15px;
|
||||
--object-radius-small: 5px;
|
||||
}
|
||||
|
||||
/* ============================================== */
|
||||
/* 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 overrides */
|
||||
/* ============================================== */
|
||||
/* DARK THEME */
|
||||
/* ============================================== */
|
||||
.dark-theme {
|
||||
--color-bg: #181818; /* war vorher surface */
|
||||
--color-surface: #1E1D1A; /* war vorher bg */
|
||||
--color-bg: #181818;
|
||||
--color-surface: #1E1D1A;
|
||||
--color-text: #E5E7EB;
|
||||
|
||||
--color-primary: #C7C5B2;
|
||||
|
|
@ -62,5 +103,37 @@
|
|||
--color-gray: #181818;
|
||||
--color-gray-hover: #2E2E2E;
|
||||
--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;
|
||||
}
|
||||
|
||||
|
|
@ -1,6 +1,33 @@
|
|||
import { defineConfig, loadEnv } from 'vite';
|
||||
import { defineConfig, loadEnv, Plugin } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
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 }) => {
|
||||
// Load env file based on mode
|
||||
|
|
@ -8,13 +35,23 @@ export default defineConfig(({ mode }) => {
|
|||
|
||||
return {
|
||||
plugins: [
|
||||
// Serve static HTML files
|
||||
serveStaticHtml(),
|
||||
react(),
|
||||
createHtmlPlugin({
|
||||
inject: {
|
||||
data: {
|
||||
VITE_APP_NAME: env.VITE_APP_NAME || 'PowerOn',
|
||||
// Only process main index.html, not public static files
|
||||
pages: [
|
||||
{
|
||||
entry: 'src/main.tsx',
|
||||
filename: 'index.html',
|
||||
template: 'index.html',
|
||||
injectOptions: {
|
||||
data: {
|
||||
VITE_APP_NAME: env.VITE_APP_NAME || 'PowerOn',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
envPrefix: 'VITE_',
|
||||
|
|
@ -23,5 +60,7 @@ export default defineConfig(({ mode }) => {
|
|||
scopeBehaviour: 'local', // Default behavior for CSS modules
|
||||
},
|
||||
},
|
||||
// Ensure public files are served correctly as static
|
||||
publicDir: 'public',
|
||||
};
|
||||
});
|
||||
|
|
|
|||