diff --git a/docs/PR_REPORT_20260126pm.md b/docs/PR_REPORT_20260126pm.md new file mode 100644 index 0000000..e3bbf72 --- /dev/null +++ b/docs/PR_REPORT_20260126pm.md @@ -0,0 +1,246 @@ +# PR-Bericht: frontend_nyla (feat/saas-multi-tenant-mandates) + +**Ziel-Branch:** `int` +**Feature-Branch:** `feat/saas-multi-tenant-mandates` +**Stand:** Januar 2026 + +--- + +## Übersicht + +| Metrik | Wert | +|--------|------| +| **Anzahl Commits** | 73 | +| **Geänderte Dateien** | 455 | +| **Zeilen hinzugefügt** | ~78'604 | +| **Zeilen gelöscht** | ~15'070 | + +--- + +## Wichtigste inhaltliche Änderungen + +### 1. SaaS Multi-Mandate & Mandatsverwaltung +- Mandaten-Navigation und Mandatenwechsel im UI +- Mandats-Einladungen und Benachrichtigungssystem +- Mandats-Rollen und -Berechtigungen (RBAC) +- Zugriff auf Seiten und Features mandatenbasiert +- UID-/ID-Mapping und Referenzen für Multi-Mandate angepasst + +### 2. Admin-Bereich +- Admin-Seiten: Mandate, RBAC-Rollen, RBAC-Regeln, Team-Mitglieder +- Feature-Zugriff, Benutzer-Zugriff, Mandats-Rollen-Berechtigungen +- Einladungsverwaltung, Benutzer-Mandate, Benutzerübersicht +- RBAC-Export/Import + +### 3. Trustee (Treuhand)-Feature +- Trustee-Views: Dashboard, Positionen, Dokumente, Verträge, Rollen, Organisationen, Access +- Expense-Import-View, Positions-Dokumente +- Trustee-API und Hooks (`useTrustee`, `useTrusteeOptions`) + +### 4. Workflows & Automation +- Workflow-Playground mit zentralem State, Log-Polling, Lifecycle-Hooks +- Automations-Seite und Workflows-Seite +- Workflow-Statistiken, verbesserte Log-Darstellung +- Chatbot-Integration (Messages, Dateien, Lifecycle) + +### 5. PEK (Projekte & Stammdaten) +- Isoliert in Folder + +### 6. Authentifizierung & Nutzerverwaltung +- Magic-Link-Login +- Passwort-Reset (Request & Reset-Seiten) +- Registrierung und Login überarbeitet +- Erweiterter Auth-Hook und CSRF-Utilities + +### 7. GDPR & Rechtliches +- GDPR-konforme Seite/Flows +- PowerOn Home, Datenschutz, AGB als statische HTML-Seiten + +### 8. UI/UX & Architektur +- Neues Page-Management (`core/PageManager`) mit datengetriebenen Seiten +- FormGenerator: Aufteilung in Form, List, Table, Controls, Action-Buttons +- Server-seitige Filter/Sort für generische Tabellen, Scroll-Lock für Header +- Sidebar überarbeitet, Mandaten-Navigation, Tree-Navigation, UserSection +- Notification-Bell, Access-Rules-Editor +- Layouts: `MainLayout`, `FeatureLayout`; Seiten: `FeatureView`, `Dashboard`, `Settings` +- Diverse UI-Komponenten (Log, Messages, MapView, Tabs, Toast, ViewForm, WorkflowStatus, etc.) + +### 9. Konfiguration & Deployment +- Konfiguration in `config/` (env, serverConfig, universalConfig) +- Scripts nach `scripts/` (server, deploy-server) +- GitHub-Workflows angepasst, Node-Version 20 + +### 10. Weitere Features & Fixes +- Speech-Integration (Prototyp), Speech-Seiten & -Transkripte +- Real-Estate/Privilege-Checker isoliert in Folder +- Althaus-Seite, PEK-Tabs umbenannt (Projects, Data Management) +- Einstellungen-Seite, Basedata-Seiten (Connections, Files, Prompts) +- Privilege-Caching und -Checker konsolidiert +- Diverse Build- und TypeScript-Fixes, Hotfixes + +--- + +## Geänderte Dateien (nach Bereichen) + +### Konfiguration & Projekt +- `.cursorignore`, `.gitignore`, `.github/workflows/*.yml` +- `README.md`, `index.html`, `package.json`, `package-lock.json` +- `vite.config.ts` +- `config/*` (neu), `.env.*` → `config/.env.*` +- `scripts/server.js`, `scripts/deploy-server.js` +- `public/`: Favicon, Logos, `poweron-home.html`, `poweron-privacy.html`, `poweron-terms.html` + +### API-Schicht +- `src/api.ts` (erweitert) +- Neu: `src/api/attributesApi.ts`, `authApi.ts`, `automationApi.ts`, `chatbotApi.ts`, `connectionApi.ts`, `featuresApi.ts`, `fileApi.ts`, `mandateApi.ts`, `permissionApi.ts`, `promptApi.ts`, `rbacRulesApi.ts`, `roleApi.ts`, `trusteeApi.ts`, `userApi.ts`, `workflowApi.ts` + +### Auth & Provider +- `src/providers/auth/AuthProvider.tsx`, `ProtectedRoute.tsx`, `authConfig.ts` +- `src/providers/language/LanguageContext.tsx` +- `src/auth/ProtectedRoute.tsx` (entfernt/ersetzt) + +### Core: PageManager +- `src/core/PageManager/PageManager.tsx`, `PageRenderer.tsx`, `SidebarProvider.tsx`, `pageInterface.ts` +- `src/core/PageManager/data/pages/*`: admin (mandates, rbac-role, rbac-rules, team-members), automations, chatbot, connections, dashboard, files, pek, pek-tables, prompts, settings, speech, speech-transcripts, trustee (access, contracts, documents, organisations, positions, roles), workflows +- `src/config/pageRegistry.tsx` + +### Komponenten (Auswahl) +- **AccessRules:** AccessLevelSelect, AccessRulesEditor, AccessRulesTable + Styles +- **FormGenerator:** FormGeneratorControls, FormGeneratorForm, FormGeneratorList, FormGeneratorTable, ActionButtons (Copy, Custom, Delete, Download, Edit, Remove, View) +- **Navigation:** MandateNavigation, TreeNavigation, UserSection + Styles +- **NotificationBell**, **ContentPreview** (inkl. Renderer) +- **Sidebar:** Sidebar, SidebarItem, SidebarSubmenu, SidebarUser, Styles, Logic, Types +- **Dashboard/DashboardChat:** teils entfernt/umgebaut (Playground-basiert) +- **Connections, Dateien, Mitglieder, PageManager, Popup, Prompts:** entfernt oder nach core/Pages migriert +- **Speech:** SpeechConfirmation, SpeechInfo, SpeechSettings, SpeechSignUp +- **TestSharepoint:** Tabelle, Logic, Interfaces +- **Workflows:** WorkflowsTable, workflowsLogic, workflowsTypes +- **UiComponents:** AutoScroll, Button, ConnectedFilesList, CopyableTruncatedValue, DragDropOverlay, DropdownSelect, InfoMessageOverlay, LocationInput, Log/LogMessage, MapView, Messages/ChatMessages, ParcelInfoPanel, Popup, Tabs, TextField, Toast, ViewForm, VoiceLanguageSelect, WorkflowStatus +- **settings:** settingsUser + +### Hooks +- Neu/erweitert: `useAccessRules`, `useAdminMandates`, `useAdminRbacRoles`, `useAdminRbacRules`, `useAuthentication`, `useAutomations`, `useChatbot`, `useConnections`, `useCurrentInstance`, `useFeatureAccess`, `useFiles`, `useInstancePermissions`, `useInvitations`, `useMandateRoles`, `useMandates`, `useNavigation`, `useNotifications`, `usePek`, `usePekTables`, `usePermissions`, `usePlayground`, `usePrompts`, `useRbacExportImport`, `useResizablePanels`, `useRoles`, `useSettings`, `useTrustee`, `useTrusteeOptions`, `useUserMandates`, `useUsers`, `useWorkflows` +- Playground: `useDashboardInputForm`, `useDashboardLogTree`, `useWorkflowLifecycle`, `useWorkflowPolling`, `useWorkflows`, `playgroundUtils` +- Entfernt: `useSharePointTest` + +### Seiten +- **Neu:** `Dashboard.tsx`, `FeatureView.tsx`, `GDPR.tsx`, `InvitePage.tsx`, `PasswordResetRequest.tsx`, `Reset.tsx`, `Settings.tsx` +- **Login, Register:** überarbeitet +- **Home:** Connections, Dashboard, Dateien, Einstellungen, Prompts, TeamBereich, TestSharepoint, Workflows entfernt/ausgelagert; `Home.tsx` angepasst +- **admin/:** AdminFeatureAccessPage, AdminFeatureInstanceUsersPage, AdminFeatureRolesPage, AdminInvitationsPage, AdminMandateRolePermissionsPage, AdminMandateRolesPage, AdminMandatesPage, AdminUserAccessOverviewPage, AdminUserMandatesPage, AdminUsersPage +- **basedata/:** ConnectionsPage, FilesPage, PromptsPage +- **migrate/:** ChatbotPage, PekPage, SpeechPage +- **views/trustee/:** TrusteeDashboardView, TrusteeDocumentsView, TrusteeExpenseImportView, TrusteeInstanceRolesView, TrusteePositionDocumentsView, TrusteePositionsView +- **workflows/:** AutomationsPage, PlaygroundPage, WorkflowsPage + +### Contexts, Stores, Utils, Locales +- **Contexts:** FileContext, PekContext, PekTablesContext, ToastContext, WorkflowSelectionContext +- **Stores:** featureStore +- **Utils:** attributeTypeMapper, csrfUtils, privilegeCheckers, time, userCache +- **Types:** mandate.ts +- **Locales:** de.ts, en.ts, fr.ts (erweitert) + +### Styles & Assets +- `src/styles/`: buttons.css, pages.module.css, themes (dark, light), assets/bg.jpg +- `src/assets/styles/light.css` entfernt +- `src/index.css`, `src/main.tsx` + +### Dokumentation +- `docs/DASHBOARD_LOG_POLLING_DOCUMENTATION.md` (neu) +- `documentation/sidebar.md` (entfernt) +- `.cursor/plans/implement_rbac_roles_page_*.plan.md` (Cursor-Plan) + +--- + +## Commit-Historie (chronologisch) + +| Datum | Commit | Nachricht | +|-------|--------|-----------| +| 2025-09-10 | 0bd6091 | updated cofig logic | +| 2025-09-15 | 9fc33c7 | feat: added speech integration prototype | +| 2025-09-16 | 9e7c3b2 | implemented feedback | +| 2025-09-18 | 41aa0fd | minor bugfixing | +| 2025-10-01 | 05f51c4 | working on action button | +| 2025-10-08 | b238ab8 | fixed action buttons | +| 2025-10-12 | 6988984 | finished files page | +| 2025-10-12 | 9519fed | pushing to int | +| 2025-10-12 | 8a0e5f8 | fix: ready for build | +| 2025-12-01 | 101b306 | added PEK pages | +| 2025-12-01 | aa34508 | updated the table view | +| 2025-12-15 | aaf64b8 | resumed backend integration, RBAC focus | +| 2025-12-15 | f8d5c0a | fix: geolinien outline | +| 2025-12-15 | 78889bf | fix: collapsed sidebar | +| 2025-12-22 | bfbe3f8 | PEK updates | +| 2025-12-30 | cf76e89 | pek update | +| 2025-12-30 | 94e8681 | fix: centralized workflow state management on dashboard page | +| 2025-12-30 | 14273c2 | Merge PR #2 feat/real-estate | +| 2025-12-30 | d3c950d | fix: consolidated privilegechecker and usepermissions hook | +| 2026-01-02 | 641930b | updated log rendering | +| 2026-01-02 | 401c088 | fix: fixed styling of log messages | +| 2026-01-02 | ae6a634 | feature: show workflow stats | +| 2026-01-05 | c76e7ef | feat: completely build up althaus page | +| 2026-01-05 | 079d398 | fix: build errors removed | +| 2026-01-05 | 6315c9a | fix: constant reload | +| 2026-01-05 | 6c90a00 | fix: another build error | +| 2026-01-05 | 05508cc | fix: privilege caching led to no pages showing | +| 2026-01-05 | 826eead | fix: added more rolelabel logging | +| 2026-01-05 | 48754d6 | fix: typescript build errors | +| 2026-01-05 | fc55a25 | fix: added more rolelabel logging | +| 2026-01-05 | 5f22c7b | fix: added more rolelabel logging | +| 2026-01-05 | eb280db | feat: completely build up althaus page | +| 2026-01-05 | c80ad96 | Rename PEK tabs: Projects and Data Management | +| 2026-01-05 | dd79895 | Merge int | +| 2026-01-05 | b0826a3 | feat: weiter chatbot implementiert | +| 2026-01-05 | 23508ea | fix: merge conflicts | +| 2026-01-05 | 350cc7b | fix: geolinien outline | +| 2026-01-05 | 407a3c4 | PEK updates | +| 2026-01-05 | c5a82dd | feat: multiselect parcels and create projects | +| 2026-01-09 | 836b803 | fix: fixed and finished chatbot integration | +| 2026-01-09 | 3df83f0 | fix: button fix | +| 2026-01-12 | 7d794ef | Merge feat/chatbot into int | +| 2026-01-12 | 5808bd4 | fixed merge conflicts | +| 2026-01-12 | 239fd32 | fix: readded deleted code, fixed build | +| 2026-01-12 | be3844f | feat: privilege checker into real estate pages | +| 2026-01-12 | eaf69f4 | feat/fix: added admin pages, fixed sidebar width | +| 2026-01-12 | 64d14af | feat: finished admin pages | +| 2026-01-12 | acdcf2c | fix: moved team-members file to correct place | +| 2026-01-12 | 06ffce8 | fix: fixed build | +| 2026-01-13 | 9fc0c9b | user magic link implemented | +| 2026-01-13 | 7d2808d | hotfix | +| 2026-01-13 | b2c38e7 | Fixed UI issues | +| 2026-01-13 | 71666d2 | hotfixes | +| 2026-01-13 | de98b86 | hotfixes | +| 2026-01-13 | c5d60c4 | node-version 20 | +| 2026-01-13 | 2b96ab7 | fixed trustee access | +| 2026-01-14 | 54ba020 | fix: fixed formgenerator layout and design | +| 2026-01-17 | 8033ca9 | prepared multimandate | +| 2026-01-20 | 70c84dd | revised ui components | +| 2026-01-21 | 7f07a55 | saas mandates core done | +| 2026-01-21 | 537b624 | fixed uid mapping to id | +| 2026-01-21 | d387322 | serverside filter and sort for form generic | +| 2026-01-21 | 34d4646 | generic form table scroll lock for headers | +| 2026-01-21 | f99b6b7 | saas multi mandate tested | +| 2026-01-22 | b207c0c | dyn options in api | +| 2026-01-23 | dc4b475 | refactored pages ui access with saas mandates | +| 2026-01-24 | cc8770d | reference fixes | +| 2026-01-24 | 41e02b5 | fixed automation and trustee | +| 2026-01-24 | 5952074 | access rules editor enhanced | +| 2026-01-24 | 6a406d8 | fixes | +| 2026-01-25 | bf4ddc6 | rbac rules tested and fixed | +| 2026-01-25 | 2b220fe | gpdr compliancy implemented | +| 2026-01-26 | 28af4cb | mandate invitation and notification system | +| 2026-01-26 | f41e6d0 | fixed ai call end to end with saas multimandate | + +--- + +## Hinweise für das PR-Review + +1. **Breite Änderung:** Viele Dateien und neue Strukturen; Fokus auf PageManager, Admin, Trustee und Workflow/Playground empfohlen. +2. **Auth & Mandate:** Magic Link, Reset-Flow und Mandatenwechsel sollten manuell geprüft werden. +3. **RBAC:** Admin RBAC-Seiten und Access Rules Editor sind kritisch für Berechtigungen. +4. **Build & Lint:** `npm run build` und Linter im CI prüfen. +5. **Doku:** `docs/DASHBOARD_LOG_POLLING_DOCUMENTATION.md` für Playground/Logging konsultieren. + +--- + +*Bericht erstellt aus der Git-Historie `main..HEAD` im Repository `frontend_nyla` (Branch `feat/saas-multi-tenant-mandates`).* diff --git a/index.html b/index.html index 54bed19..8ca96ed 100644 --- a/index.html +++ b/index.html @@ -2,9 +2,13 @@ - + <%- VITE_APP_NAME %> + + + +
diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..9d36f52 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/logos/Frame 43.png b/public/logos/Frame 43.png deleted file mode 100644 index 309d134..0000000 Binary files a/public/logos/Frame 43.png and /dev/null differ diff --git a/public/logos/PowerOn Details.PNG b/public/logos/PowerOn Details.PNG deleted file mode 100644 index d35eb4f..0000000 Binary files a/public/logos/PowerOn Details.PNG and /dev/null differ diff --git a/public/logos/PowerOn.png b/public/logos/PowerOn.png deleted file mode 100644 index ec1658a..0000000 Binary files a/public/logos/PowerOn.png and /dev/null differ diff --git a/public/logos/PowerOn_transparent.png b/public/logos/PowerOn_transparent.png deleted file mode 100644 index e8d4904..0000000 Binary files a/public/logos/PowerOn_transparent.png and /dev/null differ diff --git a/public/logos/poweron-logo.png b/public/logos/poweron-logo.png new file mode 100644 index 0000000..2a7aea3 Binary files /dev/null and b/public/logos/poweron-logo.png differ diff --git a/public/logos/spitch-logo.svg b/public/logos/spitch-logo.svg deleted file mode 100644 index ea0f59c..0000000 --- a/public/logos/spitch-logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/poweron-home.html b/public/poweron-home.html new file mode 100644 index 0000000..0a3c92a --- /dev/null +++ b/public/poweron-home.html @@ -0,0 +1,192 @@ + + + + + + + PowerOn AI Platform - Home + + + + +
+
+

PowerOn AI Platform

+

Intelligent Workflow Automation & Multi-Agent Collaboration

+
+ +
+

What is PowerOn?

+

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

+
+ +
+

Core Capabilities

+
+
+

AI Agent Management

+

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

+
+
+

Workflow Automation

+

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

+
+
+

Document Processing

+

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

+
+
+

Multi-Platform Integration

+

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

+
+
+
+ +
+

Who Benefits from PowerOn?

+

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

+
+ +
+

Key Benefits

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

Privacy Policy

+

PowerOn AI Platform - Data Protection & Privacy

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

Introduction

+

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

+
+ +
+

Information We Collect

+ +

Personal Information

+

We may collect the following types of personal information:

+ + +

Usage Information

+

We automatically collect information about how you use our platform:

+ + +

Technical Information

+

We collect technical information to ensure platform functionality:

+ +
+ +
+

How We Use Your Information

+

We use the collected information for the following purposes:

+ +
+ +
+

Data Sharing and Disclosure

+

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

+ +
+

Service Providers

+

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

+
+ +
+

Legal Requirements

+

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

+
+ +
+

Business Transfers

+

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

+
+
+ +
+

Data Security

+

We implement comprehensive security measures to protect your information:

+ +
+ +
+

Your Rights and Choices

+

You have the following rights regarding your personal information:

+ +
+ +
+

Data Retention

+

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

+ +

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

+
+ +
+

International Data Transfers

+

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

+
+ +
+

Children's Privacy

+

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

+
+ +
+

Changes to This Policy

+

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

+
+ +
+

Contact Us

+

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

+
+

Email: privacy@poweron-ai.com

+

Address: PowerOn AI Platform, Privacy Team

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

Terms of Service

+

PowerOn AI Platform - Service Agreement & User Terms

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

Acceptance of Terms

+

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

+
+ +
+

Description of Service

+

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

+

Our Platform includes the following services:

+ +
+ +
+

User Accounts and Registration

+ +

Account Creation

+

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

+ +

Account Security

+

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

+ +

Account Termination

+

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

+
+ +
+

Acceptable Use Policy

+

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

+ +
+

Prohibited Activities

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

User Content and Data

+ +

Content Ownership

+

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

+ +

Content License

+

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

+ +

Content Responsibility

+

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

+
+ +
+

Service Availability and Limitations

+ +
+

Service Availability

+

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

+
+ +
+

Service Limitations

+

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

+
+
+ +
+

Intellectual Property Rights

+

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

+
+ +
+

Third-Party Services and Integrations

+

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

+
+ +
+

Payment Terms

+ +

Pricing and Billing

+

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

+ +

Payment Obligations

+

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

+ +

Refunds

+

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

+
+ +
+

Disclaimers and Limitations of Liability

+ +
+

Service Disclaimers

+

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

+
+ +
+

Limitation of Liability

+

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

+
+
+ +
+

Indemnification

+

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

+
+ +
+

Governing Law and Dispute Resolution

+

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

+
+ +
+

Changes to Terms

+

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

+
+ +
+

Contact Information

+

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

+
+

Email: legal@poweron-ai.com

+

Address: PowerOn AI Platform, Legal Department

+
+
+ + + + +
+ + diff --git a/src/App.tsx b/src/App.tsx index de17105..0f9f8af 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,56 @@ +/** + * App.tsx + * + * Haupt-App-Komponente mit Multi-Tenant Router-Setup. + * + * URL-Struktur: + * - / → Dashboard/Übersicht + * - /settings → Benutzer-Einstellungen + * - /gdpr → GDPR / Datenschutz + * - /mandates/:mandateId/:featureCode/:instanceId/* → Feature-Instanz-Routen + * - /admin/* → System-Administration (nur SysAdmin) + */ + import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { useEffect } from 'react'; // Import global CSS reset first import './index.css'; +// Auth Pages (Public) import Login from './pages/Login'; import Register from './pages/Register'; import PasswordResetRequest from './pages/PasswordResetRequest'; import Reset from './pages/Reset'; +import { InvitePage } from './pages/InvitePage'; +// Providers import { AuthProvider } from './providers/auth/AuthProvider'; import { ProtectedRoute } from './providers/auth/ProtectedRoute'; import { LanguageProvider } from './providers/language/LanguageContext'; +import { ToastProvider } from './contexts/ToastContext'; import { WorkflowSelectionProvider } from './contexts/WorkflowSelectionContext'; import { FileProvider } from './contexts/FileContext'; -import Home from './pages/Home/Home'; + +// Layouts +import { MainLayout } from './layouts/MainLayout'; +import { FeatureLayout } from './layouts/FeatureLayout'; + +// Pages +import { DashboardPage } from './pages/Dashboard'; +import { SettingsPage } from './pages/Settings'; +import { GDPRPage } from './pages/GDPR'; +import { FeatureViewPage } from './pages/FeatureView'; +import { AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage } from './pages/admin'; + +// Workflow Pages (global) +import { PlaygroundPage, WorkflowsPage, AutomationsPage } from './pages/workflows'; + +// Basedata Pages (global) +import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata'; + +// Migrate Pages (temporary - to be migrated to feature instances) +import { ChatbotPage, PekPage, SpeechPage } from './pages/migrate'; function App() { // Load saved theme preference and set app name on app mount @@ -38,43 +74,125 @@ function App() { } document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light'); }, []); + return ( - - - {/* PUBLIC AUTH ROUTES - NO AUTHENTICATION REQUIRED */} + + + + + + {/* ================================================== */} + {/* PUBLIC AUTH ROUTES - NO AUTHENTICATION REQUIRED */} + {/* ================================================== */} } /> } /> } /> } /> + } /> - {/* PROTECTED ROUTE - requires authentication */} + {/* ================================================== */} + {/* PROTECTED ROUTES - REQUIRE AUTHENTICATION */} + {/* ================================================== */} - - - - - + - } /> + }> + {/* Dashboard (Root) */} + } /> + + {/* System-Seiten (ohne Instanz-Kontext) */} + } /> + } /> + + {/* ============================================== */} + {/* WORKFLOWS ROUTES (global) */} + {/* ============================================== */} + + } /> + } /> + } /> + + + {/* ============================================== */} + {/* BASISDATEN ROUTES (global) */} + {/* ============================================== */} + + } /> + } /> + } /> + + + {/* ============================================== */} + {/* MIGRATE TO FEATURES (temporary) */} + {/* ============================================== */} + } /> + } /> + } /> + + {/* ============================================== */} + {/* FEATURE-INSTANZ ROUTES */} + {/* /mandates/:mandateId/:featureCode/:instanceId */} + {/* ============================================== */} + } + > + {/* Feature Views - dynamisch basierend auf featureCode */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Catch-all für unbekannte Sub-Pfade */} + } /> + + + {/* ============================================== */} + {/* ADMIN ROUTES (nur SysAdmin) */} + {/* ============================================== */} + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + - {/* Catch-all redirect to home */} + {/* ================================================== */} + {/* CATCH-ALL - Redirect to Dashboard */} + {/* ================================================== */} - - - - - + } /> - - + + + + + ); } -export default App; \ No newline at end of file +export default App; diff --git a/src/api.ts b/src/api.ts index 0745a84..4a37764 100644 --- a/src/api.ts +++ b/src/api.ts @@ -20,6 +20,24 @@ const resolveHostnameToIP = async (hostname: string): Promise => } }; +/** + * Extract mandate/instance context from current URL + * URL pattern: /mandates/:mandateId/:featureCode/:instanceId/... + */ +const getContextFromUrl = (): { mandateId?: string; instanceId?: string } => { + const pathname = window.location.pathname; + const match = pathname.match(/^\/mandates\/([^/]+)\/([^/]+)\/([^/]+)/); + + if (match) { + return { + mandateId: match[1], + instanceId: match[3] + }; + } + + return {}; +}; + import { getApiBaseUrl } from '../config/config'; const api = axios.create({ @@ -27,7 +45,7 @@ const api = axios.create({ withCredentials: true }); -// Add a request interceptor to add the auth token and log backend IP +// Add a request interceptor to add the auth token, context headers, and log backend IP api.interceptors.request.use( async (config) => { // Log backend information @@ -63,6 +81,18 @@ api.interceptors.request.use( console.log('🍪 Using httpOnly cookies for authentication (automatic)'); } + // Add multi-tenant context headers from URL (if not already set) + // This ensures Feature-Instance roles are loaded for permission checks + const context = getContextFromUrl(); + if (config.headers) { + if (context.mandateId && !config.headers['X-Mandate-Id']) { + config.headers['X-Mandate-Id'] = context.mandateId; + } + if (context.instanceId && !config.headers['X-Instance-Id']) { + config.headers['X-Instance-Id'] = context.instanceId; + } + } + // Add CSRF token to all requests (including GET requests for certain endpoints) // Some endpoints like /api/realestate/* require CSRF tokens even for GET requests const method = config.method?.toLowerCase(); @@ -96,11 +126,19 @@ api.interceptors.response.use( error.config?.url?.includes('/api/local/login') || error.config?.url?.includes('/api/msft/login'); - // Don't redirect if we're already on the login page (prevents redirect loops) - const isOnLoginPage = window.location.pathname === '/login' || - window.location.pathname.startsWith('/login'); + // Don't redirect if we're on a public auth page (prevents redirect loops and allows public pages to work) + const pathname = window.location.pathname; + const isOnPublicAuthPage = pathname === '/login' || + pathname.startsWith('/login') || + pathname === '/register' || + pathname.startsWith('/register') || + pathname === '/reset' || + pathname.startsWith('/reset') || + pathname === '/password-reset-request' || + pathname.startsWith('/password-reset-request') || + pathname.startsWith('/invite'); - if (!isLoginEndpoint && !isOnLoginPage) { + if (!isLoginEndpoint && !isOnPublicAuthPage) { // Clear local auth data (httpOnly cookies are cleared by backend) sessionStorage.removeItem('auth_authority'); clearUserDataCache(); diff --git a/src/api/authApi.ts b/src/api/authApi.ts index 05075f4..8343584 100644 --- a/src/api/authApi.ts +++ b/src/api/authApi.ts @@ -85,15 +85,18 @@ export interface UsernameAvailabilityResponse { message: string; } -export interface User { +// User-Typ wird aus userApi.ts importiert +// Hier nur für Rückwärtskompatibilität +export interface AuthUser { id: string; username: string; email: string; fullName: string; language: string; enabled: boolean; - privilege: string; - mandateId: string; + roleLabels?: string[]; + authenticationAuthority: string; + isSysAdmin?: boolean; [key: string]: any; } @@ -138,7 +141,7 @@ export async function loginApi(loginData: LoginRequest): Promise * Fetch current user data * Endpoint: GET /api/local/me | /api/msft/me | /api/google/me */ -export async function fetchCurrentUserApi(authAuthority?: string): Promise { +export async function fetchCurrentUserApi(authAuthority?: string): Promise { let endpoint = '/api/local/me'; if (authAuthority === 'msft') { @@ -147,7 +150,7 @@ export async function fetchCurrentUserApi(authAuthority?: string): Promise endpoint = '/api/google/me'; } - const response = await api.get(endpoint); + const response = await api.get(endpoint); return response.data; } diff --git a/src/api/automationApi.ts b/src/api/automationApi.ts new file mode 100644 index 0000000..2004c0b --- /dev/null +++ b/src/api/automationApi.ts @@ -0,0 +1,240 @@ +import { ApiRequestOptions } from '../hooks/useApi'; + +// ============================================================================ +// TYPES & INTERFACES +// ============================================================================ + +export interface Automation { + id: string; + mandateId: string; + featureInstanceId: string; + label: string; + template: string | object; + placeholders: Record; + schedule: string; + active: boolean; + status?: string; + lastExecution?: number; + nextExecution?: number; + executionLogs?: AutomationLog[]; + _createdAt?: number; + _updatedAt?: number; + _createdByUserName?: string; + mandateName?: string; + [key: string]: any; +} + +export interface AutomationLog { + id: string; + timestamp: number; + status: string; + workflowId?: string; + messages?: string[]; +} + +export interface AutomationTemplate { + template: { + overview?: string; + tasks?: Array<{ + description?: string; + objective?: string; + [key: string]: any; + }>; + [key: string]: any; + }; + parameters?: Record; +} + +export interface CreateAutomationRequest { + label: string; + template: string; + placeholders?: Record; + schedule?: string; + active?: boolean; + mandateId?: string; + featureInstanceId?: string; +} + +export interface UpdateAutomationRequest { + label?: string; + template?: string; + placeholders?: Record; + schedule?: string; + active?: boolean; +} + +export interface ExecuteAutomationResponse { + id: string; + status: string; + workflowId?: string; + [key: string]: any; +} + +// Type for the request function passed to API functions +export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; + +// ============================================================================ +// API REQUEST FUNCTIONS +// ============================================================================ + +/** + * Fetch all automations for the current mandate + * Endpoint: GET /api/automations + */ +export async function fetchAutomations(request: ApiRequestFunction): Promise { + console.log('📤 fetchAutomations: Making API request to /api/automations'); + + try { + const data = await request({ + url: '/api/automations', + method: 'get' + }); + + console.log('📥 fetchAutomations: API response:', data); + + // Handle different response formats + let automations: Automation[] = []; + + if (Array.isArray(data)) { + automations = data; + } else if (data && typeof data === 'object') { + if (Array.isArray(data.automations)) { + automations = data.automations; + } else if (Array.isArray(data.items)) { + automations = data.items; + } else if (Array.isArray(data.data)) { + automations = data.data; + } + } + + console.log(`✅ fetchAutomations: Returning ${automations.length} automations`); + return automations; + } catch (error) { + console.error('❌ fetchAutomations: Error fetching automations:', error); + throw error; + } +} + +/** + * Fetch a single automation by ID + * Endpoint: GET /api/automations/{automationId} + */ +export async function fetchAutomation( + request: ApiRequestFunction, + automationId: string +): Promise { + return await request({ + url: `/api/automations/${automationId}`, + method: 'get' + }); +} + +/** + * Create a new automation + * Endpoint: POST /api/automations + */ +export async function createAutomationApi( + request: ApiRequestFunction, + automationData: CreateAutomationRequest +): Promise { + return await request({ + url: '/api/automations', + method: 'post', + data: automationData + }); +} + +/** + * Update an existing automation + * Endpoint: PUT /api/automations/{automationId} + */ +export async function updateAutomationApi( + request: ApiRequestFunction, + automationId: string, + updateData: UpdateAutomationRequest +): Promise { + return await request({ + url: `/api/automations/${automationId}`, + method: 'put', + data: updateData + }); +} + +/** + * Delete an automation + * Endpoint: DELETE /api/automations/{automationId} + */ +export async function deleteAutomationApi( + request: ApiRequestFunction, + automationId: string +): Promise { + await request({ + url: `/api/automations/${automationId}`, + method: 'delete' + }); +} + +/** + * Execute an automation (test mode) + * Endpoint: POST /api/automations/{automationId}/execute + */ +export async function executeAutomationApi( + request: ApiRequestFunction, + automationId: string +): Promise { + return await request({ + url: `/api/automations/${automationId}/execute`, + method: 'post' + }); +} + +/** + * Fetch automation templates + * Endpoint: GET /api/automations/templates + */ +export async function fetchAutomationTemplates( + request: ApiRequestFunction +): Promise { + const data = await request({ + url: '/api/automations/templates', + method: 'get' + }); + + if (Array.isArray(data)) { + return data; + } + + if (data && typeof data === 'object') { + if (Array.isArray(data.sets)) { + return data.sets; + } + if (Array.isArray(data.templates)) { + return data.templates; + } + } + + return []; +} + +/** + * Fetch automation attributes for dynamic form generation + * Endpoint: GET /api/attributes/AutomationDefinition + */ +export async function fetchAutomationAttributes( + request: ApiRequestFunction +): Promise { + const data = await request({ + url: '/api/attributes/AutomationDefinition', + method: 'get' + }); + + if (data?.attributes && Array.isArray(data.attributes)) { + return data.attributes; + } + + if (Array.isArray(data)) { + return data; + } + + return []; +} diff --git a/src/api/featuresApi.ts b/src/api/featuresApi.ts new file mode 100644 index 0000000..8ee75fc --- /dev/null +++ b/src/api/featuresApi.ts @@ -0,0 +1,233 @@ +/** + * Features API + * + * API-Schicht für das Multi-Tenant Feature-System. + * Hauptendpoint: GET /features/my - Lädt alle Mandate + Features + Instanzen + Permissions + */ + +import api from '../api'; +import type { + FeaturesMyResponse, + Mandate, + MandateFeature, + FeatureInstance, + InstancePermissions, + AccessLevel, +} from '../types/mandate'; + +// ============================================================================= +// MOCK DATA (Temporär bis Backend bereit) +// ============================================================================= + +const MOCK_PERMISSIONS: InstancePermissions = { + tables: { + TrusteeOrganisation: { view: true, read: 'g', create: 'g', update: 'g', delete: 'n' }, + TrusteeContract: { view: true, read: 'g', create: 'g', update: 'm', delete: 'n' }, + TrusteeDocument: { view: true, read: 'g', create: 'g', update: 'm', delete: 'm' }, + TrusteePosition: { view: true, read: 'g', create: 'g', update: 'm', delete: 'n' }, + }, + views: { + 'trustee-dashboard': true, + 'trustee-organisations': true, + 'trustee-contracts': true, + 'trustee-documents': true, + 'trustee-positions': true, + 'trustee-roles': true, + 'trustee-access': true, + }, +}; + +const MOCK_CUSTOMER_PERMISSIONS: InstancePermissions = { + tables: { + TrusteeOrganisation: { view: true, read: 'm', create: 'n', update: 'n', delete: 'n' }, + TrusteeContract: { view: true, read: 'm', create: 'n', update: 'n', delete: 'n' }, + TrusteeDocument: { view: true, read: 'm', create: 'm', update: 'm', delete: 'n' }, + TrusteePosition: { view: true, read: 'm', create: 'n', update: 'n', delete: 'n' }, + }, + views: { + 'trustee-dashboard': true, + 'trustee-contracts': true, + 'trustee-documents': true, + 'trustee-positions': true, + 'trustee-organisations': false, + 'trustee-roles': false, + 'trustee-access': false, + }, +}; + +const MOCK_WORKFLOW_PERMISSIONS: InstancePermissions = { + tables: { + WorkflowRun: { view: true, read: 'g', create: 'g', update: 'm', delete: 'n' }, + WorkflowFile: { view: true, read: 'g', create: 'g', update: 'm', delete: 'm' }, + }, + views: { + 'chatworkflow-dashboard': true, + 'chatworkflow-runs': true, + 'chatworkflow-files': true, + }, +}; + +const MOCK_RESPONSE: FeaturesMyResponse = { + mandates: [ + { + id: 'mand-soha', + name: 'Soha Treuhand', + code: 'soha', + features: [ + { + code: 'trustee', + label: { de: 'Treuhand', en: 'Trustee' }, + icon: 'briefcase', + instances: [ + { + id: 'inst-soha-pamo', + featureCode: 'trustee', + mandateId: 'mand-soha', + mandateName: 'Soha Treuhand', + instanceLabel: 'PamoCreate AG', + userRole: 'admin', + permissions: MOCK_PERMISSIONS, + }, + { + id: 'inst-soha-valueon', + featureCode: 'trustee', + mandateId: 'mand-soha', + mandateName: 'Soha Treuhand', + instanceLabel: 'ValueOn AG', + userRole: 'customer', + permissions: MOCK_CUSTOMER_PERMISSIONS, + }, + ], + }, + { + code: 'chatworkflow', + label: { de: 'Workflow', en: 'Workflow' }, + icon: 'play_circle', + instances: [ + { + id: 'inst-soha-workflow', + featureCode: 'chatworkflow', + mandateId: 'mand-soha', + mandateName: 'Soha Treuhand', + instanceLabel: 'Beratung Dynamic', + userRole: 'user', + permissions: MOCK_WORKFLOW_PERMISSIONS, + }, + ], + }, + ], + }, + { + id: 'mand-swiss', + name: 'SwissTreu', + code: 'swisstreu', + features: [ + { + code: 'trustee', + label: { de: 'Treuhand', en: 'Trustee' }, + icon: 'briefcase', + instances: [ + { + id: 'inst-swiss-firma-x', + featureCode: 'trustee', + mandateId: 'mand-swiss', + mandateName: 'SwissTreu', + instanceLabel: 'Firma X', + userRole: 'customer', + permissions: MOCK_CUSTOMER_PERMISSIONS, + }, + ], + }, + ], + }, + ], +}; + +// Flag für Mock-Modus (auf false setzen wenn Backend bereit) +const USE_MOCK = false; + +// ============================================================================= +// API FUNCTIONS +// ============================================================================= + +/** + * Lädt alle Mandate + Features + Instanzen + Permissions für den aktuellen User + * + * Endpoint: GET /api/features/my + * + * Response enthält: + * - Alle Mandanten zu denen der User Zugriff hat + * - Pro Mandant: Alle Features mit deren Instanzen + * - Pro Instanz: Summarische Berechtigungen (tables, views) + */ +export async function fetchMyFeatures(): Promise { + if (USE_MOCK) { + console.log('📦 featuresApi: Using MOCK data'); + // Simuliere Netzwerk-Latenz + await new Promise(resolve => setTimeout(resolve, 300)); + return MOCK_RESPONSE; + } + + try { + console.log('📡 featuresApi: Fetching /api/features/my'); + const response = await api.get('/api/features/my'); + console.log('✅ featuresApi: Loaded features:', { + mandateCount: response.data.mandates.length, + totalInstances: response.data.mandates + .flatMap(m => m.features) + .flatMap(f => f.instances) + .length, + }); + return response.data; + } catch (error) { + console.error('❌ featuresApi: Error fetching features:', error); + throw error; + } +} + +/** + * Lädt die verfügbaren Features (für Admin - Feature-Instanz erstellen) + * + * Endpoint: GET /api/features/available + */ +export async function fetchAvailableFeatures(): Promise { + if (USE_MOCK) { + return [ + { code: 'trustee', label: { de: 'Treuhand', en: 'Trustee' }, icon: 'briefcase', instances: [] }, + { code: 'chatworkflow', label: { de: 'Workflow', en: 'Workflow' }, icon: 'play_circle', instances: [] }, + { code: 'chatbot', label: { de: 'Chatbot', en: 'Chatbot' }, icon: 'chat', instances: [] }, + ]; + } + + const response = await api.get('/api/features/available'); + return response.data; +} + +// ============================================================================= +// TYPE GUARDS +// ============================================================================= + +export function isValidAccessLevel(value: string): value is AccessLevel { + return ['n', 'm', 'g', 'a'].includes(value); +} + +export function isValidMandate(obj: unknown): obj is Mandate { + if (!obj || typeof obj !== 'object') return false; + const mandate = obj as Record; + return ( + typeof mandate.id === 'string' && + typeof mandate.name === 'string' && + Array.isArray(mandate.features) + ); +} + +export function isValidFeatureInstance(obj: unknown): obj is FeatureInstance { + if (!obj || typeof obj !== 'object') return false; + const instance = obj as Record; + return ( + typeof instance.id === 'string' && + typeof instance.featureCode === 'string' && + typeof instance.mandateId === 'string' && + typeof instance.instanceLabel === 'string' + ); +} diff --git a/src/api/promptApi.ts b/src/api/promptApi.ts index 6204f51..3b5a000 100644 --- a/src/api/promptApi.ts +++ b/src/api/promptApi.ts @@ -62,15 +62,16 @@ export interface PaginatedResponse { } export interface CreatePromptData { - mandateId: string; name: string; content: string; + // mandateId wird nicht mehr vom Client gesendet + // Das Backend bestimmt den Kontext über die instanceId } export interface UpdatePromptData { - mandateId: string; name: string; content: string; + // mandateId wird nicht mehr vom Client gesendet } // Type for the request function passed to API functions diff --git a/src/api/trusteeApi.ts b/src/api/trusteeApi.ts index 013eeb6..a95c2ad 100644 --- a/src/api/trusteeApi.ts +++ b/src/api/trusteeApi.ts @@ -1,3 +1,12 @@ +/** + * Trustee API + * + * API-Funktionen für das Trustee-Feature. + * Alle Endpunkte erfordern eine instanceId für den Feature-Instanz-Kontext. + * + * URL-Struktur: /api/trustee/{instanceId}/{entity} + */ + import { ApiRequestOptions } from '../hooks/useApi'; // ============================================================================ @@ -60,7 +69,7 @@ export interface TrusteeDocument { contractId: string; documentName: string; documentMimeType: string; - documentData?: any; // Binary data, typically not included in list responses + documentData?: any; mandateId?: string; _createdAt?: number; _modifiedAt?: number; @@ -150,16 +159,24 @@ function _buildPaginationParams(params?: PaginationParams): Record return requestParams; } +/** + * Erstellt die Basis-URL für Trustee-Endpunkte + */ +function _getTrusteeBaseUrl(instanceId: string): string { + return `/api/trustee/${instanceId}`; +} + // ============================================================================ // ORGANISATION API // ============================================================================ export async function fetchOrganisations( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteeOrganisation[]> { return await request({ - url: '/api/trustee/organisations', + url: `${_getTrusteeBaseUrl(instanceId)}/organisations`, method: 'get', params: _buildPaginationParams(params) }); @@ -167,11 +184,12 @@ export async function fetchOrganisations( export async function fetchOrganisationById( request: ApiRequestFunction, + instanceId: string, orgId: string ): Promise { try { return await request({ - url: `/api/trustee/organisations/${orgId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`, method: 'get' }); } catch (error: any) { @@ -182,10 +200,11 @@ export async function fetchOrganisationById( export async function createOrganisation( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/organisations', + url: `${_getTrusteeBaseUrl(instanceId)}/organisations`, method: 'post', data }); @@ -193,11 +212,12 @@ export async function createOrganisation( export async function updateOrganisation( request: ApiRequestFunction, + instanceId: string, orgId: string, data: Partial ): Promise { return await request({ - url: `/api/trustee/organisations/${orgId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`, method: 'put', data }); @@ -205,10 +225,11 @@ export async function updateOrganisation( export async function deleteOrganisation( request: ApiRequestFunction, + instanceId: string, orgId: string ): Promise { await request({ - url: `/api/trustee/organisations/${orgId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`, method: 'delete' }); } @@ -219,10 +240,11 @@ export async function deleteOrganisation( export async function fetchRoles( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteeRole[]> { return await request({ - url: '/api/trustee/roles', + url: `${_getTrusteeBaseUrl(instanceId)}/roles`, method: 'get', params: _buildPaginationParams(params) }); @@ -230,11 +252,12 @@ export async function fetchRoles( export async function fetchRoleById( request: ApiRequestFunction, + instanceId: string, roleId: string ): Promise { try { return await request({ - url: `/api/trustee/roles/${roleId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`, method: 'get' }); } catch (error: any) { @@ -245,10 +268,11 @@ export async function fetchRoleById( export async function createRole( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/roles', + url: `${_getTrusteeBaseUrl(instanceId)}/roles`, method: 'post', data }); @@ -256,11 +280,12 @@ export async function createRole( export async function updateRole( request: ApiRequestFunction, + instanceId: string, roleId: string, data: Partial ): Promise { return await request({ - url: `/api/trustee/roles/${roleId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`, method: 'put', data }); @@ -268,10 +293,11 @@ export async function updateRole( export async function deleteRole( request: ApiRequestFunction, + instanceId: string, roleId: string ): Promise { await request({ - url: `/api/trustee/roles/${roleId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`, method: 'delete' }); } @@ -282,10 +308,11 @@ export async function deleteRole( export async function fetchAccess( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteeAccess[]> { return await request({ - url: '/api/trustee/access', + url: `${_getTrusteeBaseUrl(instanceId)}/access`, method: 'get', params: _buildPaginationParams(params) }); @@ -293,11 +320,12 @@ export async function fetchAccess( export async function fetchAccessById( request: ApiRequestFunction, + instanceId: string, accessId: string ): Promise { try { return await request({ - url: `/api/trustee/access/${accessId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`, method: 'get' }); } catch (error: any) { @@ -308,30 +336,33 @@ export async function fetchAccessById( export async function fetchAccessByOrganisation( request: ApiRequestFunction, + instanceId: string, orgId: string ): Promise { return await request({ - url: `/api/trustee/access/organisation/${orgId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/access/organisation/${orgId}`, method: 'get' }); } export async function fetchAccessByUser( request: ApiRequestFunction, + instanceId: string, userId: string ): Promise { return await request({ - url: `/api/trustee/access/user/${userId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/access/user/${userId}`, method: 'get' }); } export async function createAccess( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/access', + url: `${_getTrusteeBaseUrl(instanceId)}/access`, method: 'post', data }); @@ -339,11 +370,12 @@ export async function createAccess( export async function updateAccess( request: ApiRequestFunction, + instanceId: string, accessId: string, data: Partial ): Promise { return await request({ - url: `/api/trustee/access/${accessId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`, method: 'put', data }); @@ -351,10 +383,11 @@ export async function updateAccess( export async function deleteAccess( request: ApiRequestFunction, + instanceId: string, accessId: string ): Promise { await request({ - url: `/api/trustee/access/${accessId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`, method: 'delete' }); } @@ -365,10 +398,11 @@ export async function deleteAccess( export async function fetchContracts( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteeContract[]> { return await request({ - url: '/api/trustee/contracts', + url: `${_getTrusteeBaseUrl(instanceId)}/contracts`, method: 'get', params: _buildPaginationParams(params) }); @@ -376,11 +410,12 @@ export async function fetchContracts( export async function fetchContractById( request: ApiRequestFunction, + instanceId: string, contractId: string ): Promise { try { return await request({ - url: `/api/trustee/contracts/${contractId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`, method: 'get' }); } catch (error: any) { @@ -391,20 +426,22 @@ export async function fetchContractById( export async function fetchContractsByOrganisation( request: ApiRequestFunction, + instanceId: string, orgId: string ): Promise { return await request({ - url: `/api/trustee/contracts/organisation/${orgId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/contracts/organisation/${orgId}`, method: 'get' }); } export async function createContract( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/contracts', + url: `${_getTrusteeBaseUrl(instanceId)}/contracts`, method: 'post', data }); @@ -412,11 +449,12 @@ export async function createContract( export async function updateContract( request: ApiRequestFunction, + instanceId: string, contractId: string, data: Partial ): Promise { return await request({ - url: `/api/trustee/contracts/${contractId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`, method: 'put', data }); @@ -424,10 +462,11 @@ export async function updateContract( export async function deleteContract( request: ApiRequestFunction, + instanceId: string, contractId: string ): Promise { await request({ - url: `/api/trustee/contracts/${contractId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`, method: 'delete' }); } @@ -438,10 +477,11 @@ export async function deleteContract( export async function fetchDocuments( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteeDocument[]> { return await request({ - url: '/api/trustee/documents', + url: `${_getTrusteeBaseUrl(instanceId)}/documents`, method: 'get', params: _buildPaginationParams(params) }); @@ -449,11 +489,12 @@ export async function fetchDocuments( export async function fetchDocumentById( request: ApiRequestFunction, + instanceId: string, documentId: string ): Promise { try { return await request({ - url: `/api/trustee/documents/${documentId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`, method: 'get' }); } catch (error: any) { @@ -464,32 +505,54 @@ export async function fetchDocumentById( export async function fetchDocumentsByContract( request: ApiRequestFunction, + instanceId: string, contractId: string ): Promise { return await request({ - url: `/api/trustee/documents/contract/${contractId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/documents/contract/${contractId}`, method: 'get' }); } export async function createDocument( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { + // If documentData is a File, convert to base64 + let processedData = { ...data }; + if (data.documentData instanceof File) { + const file = data.documentData as File; + const arrayBuffer = await file.arrayBuffer(); + const base64 = btoa( + new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), '') + ); + processedData.documentData = base64 as any; + // Auto-set MIME type from file if not provided + if (!processedData.documentMimeType && file.type) { + processedData.documentMimeType = file.type; + } + // Auto-set name from file if not provided + if (!processedData.documentName && file.name) { + processedData.documentName = file.name; + } + } + return await request({ - url: '/api/trustee/documents', + url: `${_getTrusteeBaseUrl(instanceId)}/documents`, method: 'post', - data + data: processedData }); } export async function updateDocument( request: ApiRequestFunction, + instanceId: string, documentId: string, data: Partial ): Promise { return await request({ - url: `/api/trustee/documents/${documentId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`, method: 'put', data }); @@ -497,10 +560,11 @@ export async function updateDocument( export async function deleteDocument( request: ApiRequestFunction, + instanceId: string, documentId: string ): Promise { await request({ - url: `/api/trustee/documents/${documentId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`, method: 'delete' }); } @@ -511,10 +575,11 @@ export async function deleteDocument( export async function fetchPositions( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteePosition[]> { return await request({ - url: '/api/trustee/positions', + url: `${_getTrusteeBaseUrl(instanceId)}/positions`, method: 'get', params: _buildPaginationParams(params) }); @@ -522,11 +587,12 @@ export async function fetchPositions( export async function fetchPositionById( request: ApiRequestFunction, + instanceId: string, positionId: string ): Promise { try { return await request({ - url: `/api/trustee/positions/${positionId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`, method: 'get' }); } catch (error: any) { @@ -537,30 +603,33 @@ export async function fetchPositionById( export async function fetchPositionsByContract( request: ApiRequestFunction, + instanceId: string, contractId: string ): Promise { return await request({ - url: `/api/trustee/positions/contract/${contractId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/positions/contract/${contractId}`, method: 'get' }); } export async function fetchPositionsByOrganisation( request: ApiRequestFunction, + instanceId: string, orgId: string ): Promise { return await request({ - url: `/api/trustee/positions/organisation/${orgId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/positions/organisation/${orgId}`, method: 'get' }); } export async function createPosition( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/positions', + url: `${_getTrusteeBaseUrl(instanceId)}/positions`, method: 'post', data }); @@ -568,11 +637,12 @@ export async function createPosition( export async function updatePosition( request: ApiRequestFunction, + instanceId: string, positionId: string, data: Partial ): Promise { return await request({ - url: `/api/trustee/positions/${positionId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`, method: 'put', data }); @@ -580,10 +650,11 @@ export async function updatePosition( export async function deletePosition( request: ApiRequestFunction, + instanceId: string, positionId: string ): Promise { await request({ - url: `/api/trustee/positions/${positionId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`, method: 'delete' }); } @@ -594,10 +665,11 @@ export async function deletePosition( export async function fetchPositionDocuments( request: ApiRequestFunction, + instanceId: string, params?: PaginationParams ): Promise | TrusteePositionDocument[]> { return await request({ - url: '/api/trustee/position-documents', + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`, method: 'get', params: _buildPaginationParams(params) }); @@ -605,11 +677,12 @@ export async function fetchPositionDocuments( export async function fetchPositionDocumentById( request: ApiRequestFunction, + instanceId: string, linkId: string ): Promise { try { return await request({ - url: `/api/trustee/position-documents/${linkId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`, method: 'get' }); } catch (error: any) { @@ -620,41 +693,58 @@ export async function fetchPositionDocumentById( export async function fetchDocumentsForPosition( request: ApiRequestFunction, + instanceId: string, positionId: string ): Promise { return await request({ - url: `/api/trustee/position-documents/position/${positionId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/position/${positionId}`, method: 'get' }); } export async function fetchPositionsForDocument( request: ApiRequestFunction, + instanceId: string, documentId: string ): Promise { return await request({ - url: `/api/trustee/position-documents/document/${documentId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/document/${documentId}`, method: 'get' }); } export async function createPositionDocument( request: ApiRequestFunction, + instanceId: string, data: Partial ): Promise { return await request({ - url: '/api/trustee/position-documents', + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`, method: 'post', data }); } +export async function updatePositionDocument( + request: ApiRequestFunction, + instanceId: string, + linkId: string, + data: Partial +): Promise { + return await request({ + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`, + method: 'put', + data + }); +} + export async function deletePositionDocument( request: ApiRequestFunction, + instanceId: string, linkId: string ): Promise { await request({ - url: `/api/trustee/position-documents/${linkId}`, + url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`, method: 'delete' }); } diff --git a/src/api/userApi.ts b/src/api/userApi.ts index 6cf36e6..0ba5f24 100644 --- a/src/api/userApi.ts +++ b/src/api/userApi.ts @@ -13,8 +13,10 @@ export interface User { enabled: boolean; roleLabels?: string[]; // Array of role labels from backend (e.g., ["user"]) authenticationAuthority: string; - mandateId: string; - [key: string]: any; // Allow additional properties (may include deprecated 'privilege' from backend) + isSysAdmin?: boolean; // System-Administrator Flag + // mandateId ist nicht mehr Teil des User-Objekts (Multi-Tenant-Konzept) + // Der Mandant-Kontext wird über Feature-Instanzen bestimmt + [key: string]: any; // Allow additional properties } export type UserUpdateData = Partial>; diff --git a/src/components/AccessRules/AccessLevelSelect.tsx b/src/components/AccessRules/AccessLevelSelect.tsx new file mode 100644 index 0000000..bafba16 --- /dev/null +++ b/src/components/AccessRules/AccessLevelSelect.tsx @@ -0,0 +1,59 @@ +/** + * AccessLevelSelect + * + * Dropdown component for selecting RBAC access levels (n/m/g/a). + */ + +import React from 'react'; +import { ACCESS_LEVEL_OPTIONS, type AccessLevel, getAccessLevelColor } from '../../hooks/useAccessRules'; +import styles from './AccessRules.module.css'; + +interface AccessLevelSelectProps { + value: AccessLevel | null; + onChange: (value: AccessLevel) => void; + disabled?: boolean; + label?: string; + showLabel?: boolean; + compact?: boolean; +} + +export const AccessLevelSelect: React.FC = ({ + value, + onChange, + disabled = false, + label, + showLabel = false, + compact = false, +}) => { + const currentColor = getAccessLevelColor(value); + + return ( +
+ {showLabel && label && ( + + )} + +
+ ); +}; + +export default AccessLevelSelect; diff --git a/src/components/AccessRules/AccessRules.module.css b/src/components/AccessRules/AccessRules.module.css new file mode 100644 index 0000000..7730262 --- /dev/null +++ b/src/components/AccessRules/AccessRules.module.css @@ -0,0 +1,792 @@ +/* ============================================================================= + * AccessRules Components Styles + * ============================================================================= */ + +/* ============================================================================= + * Access Level Select + * ============================================================================= */ + +.accessLevelSelect { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.accessLevelSelect.compact { + gap: 0; +} + +.accessLevelLabel { + font-size: 0.75rem; + color: var(--text-secondary); + font-weight: 500; +} + +.accessLevelDropdown { + padding: 0.375rem 0.5rem; + border: 2px solid var(--border-color); + border-radius: 4px; + background: var(--bg-primary); + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + min-width: 80px; + transition: border-color 0.2s, box-shadow 0.2s; +} + +.accessLevelDropdown:hover:not(:disabled) { + box-shadow: 0 0 0 2px var(--primary-color-light); +} + +.accessLevelDropdown:focus { + outline: none; + box-shadow: 0 0 0 2px var(--primary-color); +} + +.accessLevelDropdown:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* ============================================================================= + * Access Rules Editor + * ============================================================================= */ + +.accessRulesEditor { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1rem; + background: var(--bg-primary); + border-radius: 8px; + border: 1px solid var(--border-color); +} + +.editorHeader { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 0.75rem; + border-bottom: 1px solid var(--border-color); +} + +.editorTitle { + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.templateBadge { + background: var(--info-color); + color: white; + font-size: 0.625rem; + padding: 0.125rem 0.375rem; + border-radius: 4px; + text-transform: uppercase; + font-weight: 700; +} + +.headerActions { + display: flex; + gap: 0.5rem; +} + +/* ============================================================================= + * Tabs + * ============================================================================= */ + +.tabsContainer { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.tabList { + display: flex; + gap: 0.25rem; + border-bottom: 2px solid var(--border-color); + padding-bottom: -2px; +} + +.tab { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1rem; + background: none; + border: none; + border-bottom: 2px solid transparent; + margin-bottom: -2px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary); + transition: all 0.2s; +} + +.tab:hover { + color: var(--text-primary); + background: var(--bg-secondary); +} + +.tab.active { + color: var(--primary-color); + border-bottom-color: var(--primary-color); +} + +.tabIcon { + font-size: 1rem; +} + +.tabBadge { + background: var(--bg-tertiary); + color: var(--text-secondary); + font-size: 0.75rem; + padding: 0.125rem 0.375rem; + border-radius: 10px; + min-width: 20px; + text-align: center; +} + +.tab.active .tabBadge { + background: var(--primary-color); + color: white; +} + +.tabContent { + min-height: 200px; +} + +/* ============================================================================= + * Rules Section + * ============================================================================= */ + +.rulesSection { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.sectionHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.sectionTitle { + font-size: 0.875rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.addButton { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.75rem; + background: var(--primary-color); + color: white; + border: none; + border-radius: 4px; + font-size: 0.8125rem; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.addButton:hover { + background: var(--primary-color-dark); +} + +.addButton:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* ============================================================================= + * Rule Card + * ============================================================================= */ + +.ruleCard { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0.875rem; + background: var(--bg-secondary); + border-radius: 6px; + border: 1px solid var(--border-color); +} + +.ruleHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.ruleItem { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.ruleItemIcon { + color: var(--text-tertiary); + font-size: 0.875rem; +} + +.ruleItemName { + font-weight: 500; + color: var(--text-primary); + font-family: 'Monaco', 'Menlo', monospace; + font-size: 0.875rem; +} + +.ruleActions { + display: flex; + gap: 0.25rem; +} + +.iconButton { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: none; + border: 1px solid transparent; + border-radius: 4px; + cursor: pointer; + color: var(--text-tertiary); + transition: all 0.2s; +} + +.iconButton:hover { + background: var(--bg-tertiary); + color: var(--text-primary); + border-color: var(--border-color); +} + +.iconButton.danger:hover { + background: #fed7d7; + color: #c53030; + border-color: #fc8181; +} + +/* ============================================================================= + * Permissions Grid + * ============================================================================= */ + +.permissionsGrid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0.5rem; +} + +.permissionItem { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; +} + +.permissionLabel { + font-size: 0.6875rem; + color: var(--text-tertiary); + text-transform: uppercase; + font-weight: 500; +} + +/* View Toggle */ +.viewToggle { + display: flex; + align-items: center; + justify-content: center; + width: 100%; +} + +.viewCheckbox { + width: 18px; + height: 18px; + cursor: pointer; + accent-color: var(--primary-color); +} + +/* ============================================================================= + * Empty State + * ============================================================================= */ + +.emptyState { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 2rem; + color: var(--text-tertiary); + text-align: center; +} + +.emptyIcon { + font-size: 2rem; + margin-bottom: 0.75rem; + opacity: 0.5; +} + +.emptyText { + font-size: 0.875rem; + margin: 0; +} + +.emptyHint { + font-size: 0.75rem; + margin-top: 0.25rem; +} + +/* ============================================================================= + * Add Rule Modal + * ============================================================================= */ + +.addRuleForm { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.formGroup { + display: flex; + flex-direction: column; + gap: 0.375rem; +} + +.formLabel { + font-size: 0.8125rem; + font-weight: 500; + color: var(--text-primary); +} + +.formInput { + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 0.875rem; + background: var(--bg-primary); + color: var(--text-primary); +} + +.formInput:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px var(--primary-color-light); +} + +.formSelect { + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 0.875rem; + background: var(--bg-primary); + color: var(--text-primary); + cursor: pointer; +} + +.formHint { + font-size: 0.75rem; + color: var(--text-tertiary); +} + +.formActions { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + margin-top: 0.5rem; +} + +/* ============================================================================= + * Action Bar + * ============================================================================= */ + +.actionBar { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + padding-top: 1rem; + border-top: 1px solid var(--border-color); +} + +.secondaryButton { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0.5rem 1rem; + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.secondaryButton:hover { + background: var(--bg-tertiary); +} + +.secondaryButton:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.primaryButton { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0.5rem 1rem; + background: var(--primary-color); + color: white; + border: none; + border-radius: 4px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.primaryButton:hover { + background: var(--primary-color-dark); +} + +.primaryButton:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* ============================================================================= + * Loading State + * ============================================================================= */ + +.loadingContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem; + gap: 1rem; +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid var(--border-color); + border-top-color: var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* ============================================================================= + * JSON Editor Tab + * ============================================================================= */ + +.jsonEditor { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.jsonTextarea { + width: 100%; + min-height: 300px; + padding: 0.75rem; + border: 1px solid var(--border-color); + border-radius: 4px; + font-family: 'Monaco', 'Menlo', monospace; + font-size: 0.8125rem; + line-height: 1.5; + background: var(--bg-secondary); + color: var(--text-primary); + resize: vertical; +} + +.jsonTextarea:focus { + outline: none; + border-color: var(--primary-color); +} + +.jsonError { + color: #c53030; + font-size: 0.8125rem; + padding: 0.5rem; + background: #fed7d7; + border-radius: 4px; +} + +.jsonHint { + font-size: 0.75rem; + color: var(--text-tertiary); +} + +/* ============================================================================= + * Access Rules Table (Checkbox Matrix) + * ============================================================================= */ + +.tableWrapper { + overflow-x: auto; + margin: 0 -0.5rem; + padding: 0 0.5rem; +} + +.accessRulesTable { + width: 100%; + border-collapse: collapse; + font-size: 0.8125rem; + min-width: 800px; +} + +.accessRulesTable th, +.accessRulesTable td { + padding: 0.5rem 0.375rem; + border-bottom: 1px solid var(--border-color); + text-align: center; + vertical-align: middle; +} + +.accessRulesTable th { + background: var(--bg-secondary); + font-weight: 600; + font-size: 0.6875rem; + text-transform: uppercase; + color: var(--text-secondary); + white-space: nowrap; +} + +.accessRulesTable tbody tr:hover { + background: var(--bg-secondary); +} + +.colObject { + text-align: left !important; + min-width: 220px; + max-width: 350px; +} + +.colView { + width: 50px; +} + +.colGroupHeader { + border-left: 2px solid var(--border-color); + background: var(--bg-tertiary) !important; +} + +.colGroupHeader:nth-of-type(3) { + background: rgba(72, 187, 120, 0.1) !important; +} + +.colGroupHeader:nth-of-type(4) { + background: rgba(66, 153, 225, 0.1) !important; +} + +.colGroupHeader:nth-of-type(5) { + background: rgba(237, 100, 166, 0.1) !important; +} + +.subHeader th { + font-size: 0.625rem; + padding: 0.25rem 0.375rem; + background: var(--bg-primary) !important; + font-weight: 700; + color: var(--text-tertiary); +} + +.subHeader th:nth-child(n+3):nth-child(-n+6) { + background: rgba(72, 187, 120, 0.05) !important; +} + +.subHeader th:nth-child(n+7):nth-child(-n+10) { + background: rgba(66, 153, 225, 0.05) !important; +} + +.subHeader th:nth-child(n+11):nth-child(-n+14) { + background: rgba(237, 100, 166, 0.05) !important; +} + +.objectCell { + text-align: left !important; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.objectIcon { + color: var(--text-tertiary); + font-size: 0.75rem; + flex-shrink: 0; +} + +.objectCode { + font-family: 'Monaco', 'Menlo', 'Consolas', monospace; + font-size: 0.75rem; + background: var(--bg-tertiary); + padding: 0.125rem 0.375rem; + border-radius: 3px; + color: var(--text-primary); + word-break: break-all; +} + +.checkboxCell { + width: 32px; + padding: 0.375rem 0.25rem !important; +} + +.checkboxCell input[type="checkbox"] { + width: 15px; + height: 15px; + cursor: pointer; + accent-color: var(--primary-color); + margin: 0; +} + +.checkboxCell input[type="checkbox"]:disabled { + cursor: not-allowed; + opacity: 0.5; +} + +.actionsCell { + width: 40px; + padding: 0.375rem !important; +} + +.ruleRow td { + padding: 0.5rem 0.375rem; +} + +.ruleRow td:nth-child(n+3):nth-child(-n+6) { + background: rgba(72, 187, 120, 0.02); +} + +.ruleRow td:nth-child(n+7):nth-child(-n+10) { + background: rgba(66, 153, 225, 0.02); +} + +.ruleRow td:nth-child(n+11):nth-child(-n+14) { + background: rgba(237, 100, 166, 0.02); +} + +/* Toggle between Card and Table View */ +.viewToggleButton { + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.5rem; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 0.75rem; + cursor: pointer; + color: var(--text-secondary); + transition: all 0.2s; +} + +.viewToggleButton:hover { + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.viewToggleButton.active { + background: var(--primary-color); + color: white; + border-color: var(--primary-color); +} + +/* Object Selector */ +.objectSelector { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.objectSelectorLabel { + display: flex; + justify-content: space-between; + align-items: center; +} + +.toggleCustomButton { + padding: 0.125rem 0.5rem; + background: none; + border: 1px solid var(--border-color); + border-radius: 3px; + font-size: 0.6875rem; + cursor: pointer; + color: var(--text-secondary); +} + +.toggleCustomButton:hover { + background: var(--bg-secondary); + color: var(--text-primary); +} + +/* Add Rule Matrix (Checkbox Style) */ +.addRuleMatrix { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin-top: 0.75rem; + padding: 0.75rem; + background: var(--bg-secondary); + border-radius: 6px; + border: 1px solid var(--border-color); +} + +.matrixHeader { + display: grid; + grid-template-columns: 80px repeat(3, 1fr); + gap: 0.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border-color); +} + +.matrixGroup { + text-align: center; + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + color: var(--text-secondary); +} + +.matrixRow { + display: grid; + grid-template-columns: 80px repeat(3, 1fr); + gap: 0.5rem; + padding: 0.25rem 0; +} + +.matrixLabel { + font-size: 0.8125rem; + font-weight: 500; + color: var(--text-primary); + display: flex; + align-items: center; +} + +.matrixCell { + display: flex; + justify-content: center; + align-items: center; +} + +.matrixCell input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: var(--primary-color); +} diff --git a/src/components/AccessRules/AccessRulesEditor.tsx b/src/components/AccessRules/AccessRulesEditor.tsx new file mode 100644 index 0000000..8098eb9 --- /dev/null +++ b/src/components/AccessRules/AccessRulesEditor.tsx @@ -0,0 +1,762 @@ +/** + * AccessRulesEditor + * + * Main component for editing RBAC access rules for a role. + * Provides tabbed interface for DATA, UI, and RESOURCE rules. + * + * Features: + * - Checkbox-based compact table for DATA rules + * - Card view for UI/RESOURCE rules + * - Object catalog dropdown for adding new rules + */ + +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { + FaTable, + FaDesktop, + FaServer, + FaCode, + FaPlus, + FaTrash, + FaSave, + FaUndo, + FaSpinner, + FaThList, + FaTh, +} from 'react-icons/fa'; +import { useToast } from '../../contexts/ToastContext'; +import { + useAccessRules, + type AccessRule, + type RuleContext, + type AccessLevel, + type AccessRuleCreate, +} from '../../hooks/useAccessRules'; +import { useCatalogObjects, type CatalogObject } from '../../hooks/useCatalogObjects'; +import { AccessLevelSelect } from './AccessLevelSelect'; +import { AccessRulesTable } from './AccessRulesTable'; +import styles from './AccessRules.module.css'; + +// ============================================================================= +// TYPES +// ============================================================================= + +interface AccessRulesEditorProps { + roleId: string; + roleName?: string; + isTemplate?: boolean; + readOnly?: boolean; + onSave?: () => void; + apiBasePath?: string; + mandateId?: string; + featureCode?: string; // Filter catalog objects to this feature only +} + +type TabType = 'DATA' | 'UI' | 'RESOURCE' | 'JSON'; + +// ============================================================================= +// RULE CARD COMPONENT +// ============================================================================= + +interface RuleCardProps { + rule: AccessRule; + readOnly?: boolean; + onUpdate: (ruleId: string, updates: Partial) => void; + onDelete: (ruleId: string) => void; +} + +const RuleCard: React.FC = ({ rule, readOnly, onUpdate, onDelete }) => { + const isDataRule = rule.context === 'DATA'; + + return ( +
+
+
+ + {rule.context === 'DATA' ? : + rule.context === 'UI' ? : } + + {rule.item || '(global)'} +
+ {!readOnly && ( +
+ +
+ )} +
+ +
+ {/* View Toggle */} +
+ View +
+ onUpdate(rule.id, { view: e.target.checked })} + disabled={readOnly} + className={styles.viewCheckbox} + /> +
+
+ + {/* CRUD Levels (only for DATA context) */} + {isDataRule ? ( + <> +
+ Read + onUpdate(rule.id, { read: value })} + disabled={readOnly} + compact + /> +
+
+ Create + onUpdate(rule.id, { create: value })} + disabled={readOnly} + compact + /> +
+
+ Update + onUpdate(rule.id, { update: value })} + disabled={readOnly} + compact + /> +
+
+ Delete + onUpdate(rule.id, { delete: value })} + disabled={readOnly} + compact + /> +
+ + ) : ( + // For UI and RESOURCE, show empty placeholders to maintain grid +
+ )} +
+
+ ); +}; + +// ============================================================================= +// ADD RULE FORM +// ============================================================================= + +interface AddRuleFormProps { + context: RuleContext; + availableObjects: CatalogObject[]; + onAdd: (rule: AccessRuleCreate) => void; + onCancel: () => void; +} + +const AddRuleForm: React.FC = ({ context, availableObjects, onAdd, onCancel }) => { + const [item, setItem] = useState(''); + const [useCustom, setUseCustom] = useState(false); + const [view, setView] = useState(true); + const [read, setRead] = useState('n'); + const [create, setCreate] = useState('n'); + const [update, setUpdate] = useState('n'); + const [del, setDel] = useState('n'); + + // Group objects by feature + const groupedObjects = useMemo(() => { + const grouped: Record = {}; + availableObjects.forEach(obj => { + if (!grouped[obj.featureCode]) { + grouped[obj.featureCode] = []; + } + grouped[obj.featureCode].push(obj); + }); + return grouped; + }, [availableObjects]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const newRule: AccessRuleCreate = { + context, + item: item.trim() || null, + view, + ...(context === 'DATA' ? { read, create, update, delete: del } : {}), + }; + onAdd(newRule); + }; + + const getPlaceholder = () => { + switch (context) { + case 'DATA': + return 'z.B. data.feature.trustee.TrusteePosition'; + case 'UI': + return 'z.B. ui.feature.trustee.dashboard'; + case 'RESOURCE': + return 'z.B. resource.feature.trustee.documents.create'; + } + }; + + const getLabel = (obj: CatalogObject): string => { + return obj.label.de || obj.label.en || obj.objectKey; + }; + + return ( +
+
+
+ + +
+ + {useCustom ? ( + setItem(e.target.value)} + placeholder={getPlaceholder()} + className={styles.formInput} + autoFocus + /> + ) : ( + + )} + + + Leer lassen für globale Regel. Längster Match gewinnt bei Wildcards (z.B. data.feature.trustee.*). + +
+ +
+ +
+ + {context === 'DATA' && ( +
+ {/* Header Row */} +
+
+
Eigene (m)
+
Gruppe (g)
+
Alle (a)
+
+ + {/* CRUD Rows */} + {(['create', 'read', 'update', 'delete'] as const).map(op => { + const value = op === 'delete' ? del : op === 'create' ? create : op === 'update' ? update : read; + const setValue = op === 'delete' ? setDel : op === 'create' ? setCreate : op === 'update' ? setUpdate : setRead; + const labels = { create: 'Create', read: 'Read', update: 'Update', delete: 'Delete' }; + + return ( +
+
{labels[op]}
+ {(['m', 'g', 'a'] as const).map(level => ( +
+ { + if (e.target.checked) { + setValue(level); + } else { + // Deactivate: set to level below + const hierarchy: AccessLevel[] = ['n', 'm', 'g', 'a']; + const idx = hierarchy.indexOf(level); + setValue(hierarchy[idx - 1] || 'n'); + } + }} + title={`${labels[op]} - ${level === 'm' ? 'Eigene' : level === 'g' ? 'Gruppe' : 'Alle'}`} + /> +
+ ))} +
+ ); + })} +
+ )} + +
+ + +
+
+ ); +}; + +// ============================================================================= +// RULES SECTION +// ============================================================================= + +interface RulesSectionProps { + context: RuleContext; + rules: AccessRule[]; + availableObjects: CatalogObject[]; + readOnly?: boolean; + onUpdate: (ruleId: string, updates: Partial) => void; + onDelete: (ruleId: string) => void; + onAdd: (rule: AccessRuleCreate) => void; +} + +const RulesSection: React.FC = ({ + context, + rules, + availableObjects, + readOnly, + onUpdate, + onDelete, + onAdd, +}) => { + const [showAddForm, setShowAddForm] = useState(false); + const [useTableView, setUseTableView] = useState(context === 'DATA'); // Default to table for DATA + + const handleAdd = (rule: AccessRuleCreate) => { + onAdd(rule); + setShowAddForm(false); + }; + + const getEmptyIcon = () => { + switch (context) { + case 'DATA': return ; + case 'UI': return ; + case 'RESOURCE': return ; + } + }; + + const getEmptyText = () => { + switch (context) { + case 'DATA': return 'Keine Daten-Regeln definiert'; + case 'UI': return 'Keine UI-Regeln definiert'; + case 'RESOURCE': return 'Keine Ressourcen-Regeln definiert'; + } + }; + + return ( +
+ {!readOnly && !showAddForm && ( +
+ + {rules.length} {rules.length === 1 ? 'Regel' : 'Regeln'} + +
+ {/* View Toggle */} + {context === 'DATA' && rules.length > 0 && ( + <> + + + + )} + +
+
+ )} + + {showAddForm && ( + setShowAddForm(false)} + /> + )} + + {rules.length === 0 && !showAddForm ? ( +
+
{getEmptyIcon()}
+

{getEmptyText()}

+ {!readOnly && ( +

+ Klicken Sie auf "Neue Regel" um eine Berechtigung hinzuzufügen. +

+ )} +
+ ) : useTableView && context === 'DATA' ? ( + + ) : ( + rules.map(rule => ( + + )) + )} +
+ ); +}; + +// ============================================================================= +// JSON EDITOR +// ============================================================================= + +interface JsonEditorProps { + rules: AccessRule[]; + readOnly?: boolean; + onApply: (rules: AccessRule[]) => void; +} + +const JsonEditor: React.FC = ({ rules, readOnly, onApply }) => { + const [jsonText, setJsonText] = useState(''); + const [error, setError] = useState(null); + + useEffect(() => { + setJsonText(JSON.stringify(rules, null, 2)); + setError(null); + }, [rules]); + + const handleApply = () => { + try { + const parsed = JSON.parse(jsonText); + if (!Array.isArray(parsed)) { + throw new Error('JSON muss ein Array sein'); + } + setError(null); + onApply(parsed); + } catch (err: any) { + setError(err.message); + } + }; + + return ( +
+