ui all languages transferred
This commit is contained in:
parent
9661a0f7a5
commit
c3646075ff
146 changed files with 3118 additions and 1787 deletions
182
docs/i18n-remaining-items.md
Normal file
182
docs/i18n-remaining-items.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# i18n — Verbleibende statische Texte
|
||||
|
||||
> Stand: 2026-04-08
|
||||
> Diese Stellen verwenden noch hardcoded Strings in Object-Literalen, Arrays oder Hook-Defaults.
|
||||
> Sie können nicht einfach mit `t()` gewrapped werden, da sie ausserhalb des React-Render-Kontexts definiert sind.
|
||||
> **Lösung:** Array/Object in die Komponente verschieben oder eine Factory-Funktion `(t) => [...]` nutzen.
|
||||
|
||||
---
|
||||
|
||||
## 1. Hook-Defaults (`useConfirm`, `usePrompt`)
|
||||
|
||||
Diese Defaults propagieren in die gesamte App. Ein Fix hier wirkt global.
|
||||
|
||||
| Datei | Zeile | Property | Text |
|
||||
|-------|-------|----------|------|
|
||||
| `hooks/useConfirm.tsx` | 26 | `title` | `'Bestätigung'` |
|
||||
| `hooks/useConfirm.tsx` | 27 | `confirmLabel` | `'Bestätigen'` |
|
||||
| `hooks/useConfirm.tsx` | 28 | `cancelLabel` | `'Abbrechen'` |
|
||||
| `hooks/usePrompt.tsx` | 29 | `title` | `'Eingabe'` |
|
||||
| `hooks/usePrompt.tsx` | 30 | `confirmLabel` | `'OK'` |
|
||||
| `hooks/usePrompt.tsx` | 31 | `cancelLabel` | `'Abbrechen'` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Monatsnamen
|
||||
|
||||
| Datei | Zeilen | Kontext |
|
||||
|-------|--------|---------|
|
||||
| `pages/billing/BillingDashboard.tsx` | 182–193 | Monats-Select: `'Januar'` bis `'Dezember'` (12 Einträge) |
|
||||
| `components/FormGenerator/FormGeneratorReport/FormGeneratorReport.tsx` | 536–541 | Monats-Select: `'Januar'` bis `'Dezember'` (12 Einträge) |
|
||||
|
||||
---
|
||||
|
||||
## 3. Tab-Labels (statische Arrays ausserhalb Komponente)
|
||||
|
||||
| Datei | Zeilen | Labels |
|
||||
|-------|--------|--------|
|
||||
| `pages/Settings.tsx` | 23–27 | `'Profil'`, `'Darstellung'`, `'Stimme & Sprache'`, `'Neutralisierung (lokal)'`, `'Datenschutz'` |
|
||||
| `pages/views/workspace/WorkspaceSettingsPage.tsx` | 16–17 | `'Generelle Einstellungen'`, `'Neutralisierung (Workspace)'` |
|
||||
| `pages/views/neutralization/NeutralizationView.tsx` | 744–745 | `'Configuration'`, `'Playground'` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Spalten-Definitionen (Column-Arrays)
|
||||
|
||||
| Datei | Zeilen | Labels |
|
||||
|-------|--------|--------|
|
||||
| `pages/admin/AdminLanguagesPage.tsx` | 29–32 | `'Code'`, `'Bezeichnung'`, `'Status'`, `'Einträge'` |
|
||||
| `pages/admin/AdminSubscriptionsPage.tsx` | 13–23 | `'Mandant'`, `'Plan'`, `'Status'`, `'Wiederkehrend'`, `'User'`, `'Instanzen'`, `'Revenue/Mt (CHF)'`, `'Gestartet'`, `'Periodenende'`, `'Preis/User'`, `'Preis/Instanz'` |
|
||||
| `pages/admin/AdminUserMandatesPage.tsx` | 104–144 | `'Benutzername'`, `'E-Mail'`, `'Vollständiger Name'`, `'Rollen'`, `'Aktiv'` |
|
||||
| `pages/admin/AdminMandateRolesPage.tsx` | 106–124 | `'Bezeichnung'`, `'Beschreibung'`, `'Geltungsbereich'` |
|
||||
| `pages/admin/AdminInvitationsPage.tsx` | 90–155 | `'Benutzername'`, `'E-Mail'`, `'Rollen'`, `'Gültig bis'`, `'Verwendet'`, `'Erstellt'` |
|
||||
| `pages/admin/AdminFeatureRolesPage.tsx` | 138–155 | `'Rollen-Label'`, `'Beschreibung'`, `'Feature'` |
|
||||
| `pages/admin/AdminFeatureInstanceUsersPage.tsx` | 205–245 | `'Benutzername'`, `'E-Mail'`, `'Vollständiger Name'`, `'Rollen'`, `'Aktiv'` |
|
||||
| `pages/admin/AdminFeatureAccessPage.tsx` | 91–104 | `'Name'`, `'Feature'`, `'Aktiv'` |
|
||||
| `pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx` | 184–235 | `'Workflow'`, `'Aktiv'`, `'Läuft'`, `'Steht bei'`, `'Erstellt'`, `'Zuletzt gestartet'`, `'Läufe'` |
|
||||
| `pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx` | 174–202 | `'Vorlage'`, `'Scope'`, `'Freigegeben'`, `'Erstellt von'`, `'Erstellt'` |
|
||||
|
||||
---
|
||||
|
||||
## 5. Formular-Feld-Definitionen (AttributeDefinition-Arrays)
|
||||
|
||||
| Datei | Zeilen | Labels |
|
||||
|-------|--------|--------|
|
||||
| `pages/Settings.tsx` | 56–58 | `'Vollstaendiger Name'`, `'E-Mail-Adresse'`, `'Sprache'` + descriptions + placeholders |
|
||||
| `pages/admin/wizards/FeatureInstanceWizard.tsx` | 75–78 | `'Mandant'`, `'Feature'`, `'Bezeichnung'`, `'Aktiv'` |
|
||||
| `pages/admin/InstanceDetailModal.tsx` | 186–279 | `'Benutzer'`, `'Rollen'`, `'Aktiv'`, `'Einstellungen'`, `'Bezeichnung'`, `'Aktiviert'` |
|
||||
| `pages/admin/AdminMandateRolesPage.tsx` | 165–171 | `'Geltungsbereich'`, `'Nur dieser Mandant'`, `'Template (wird bei neuen Mandanten kopiert)'` |
|
||||
| `pages/admin/AdminMandateRolePermissionsPage.tsx` | 219–221 | `'Mandanten-Rollen'`, `'Alle (inkl. Templates)'`, `'Nur Templates'` |
|
||||
| `pages/admin/AdminFeatureRolesPage.tsx` | 173–205 | `'Rollen-Label'`, `'Beschreibung'` + descriptions |
|
||||
| `pages/admin/AdminFeatureInstanceUsersPage.tsx` | 272–299 | `'Benutzer'`, `'Rollen'`, `'Aktiv'` |
|
||||
| `pages/admin/AdminInvitationsPage.tsx` | 181 | `'Gültigkeitsdauer (Stunden)'` |
|
||||
| `pages/admin/AdminFeatureAccessPage.tsx` | 629–636 | `'Bezeichnung'`, `'Aktiviert'` |
|
||||
|
||||
---
|
||||
|
||||
## 6. Status-/Option-Maps (Object-Literale)
|
||||
|
||||
| Datei | Zeilen | Kontext |
|
||||
|-------|--------|---------|
|
||||
| `pages/billing/SubscriptionTab.tsx` | 48–53 | Status-Map: `'Zahlung ausstehend'`, `'Geplant'`, `'Aktiv'`, `'Testphase'`, `'Abgelaufen'` |
|
||||
| `components/FlowEditor/editor/CanvasHeader.tsx` | 40–42 | Status-Map: `'Entwurf'`, `'Veröffentlicht'`, `'Archiviert'` |
|
||||
| `components/FlowEditor/editor/WorkflowConfigurationModal.tsx` | 17–20 | Trigger-Typen: `'Manueller Trigger'`, `'Formular'`, `'Zeitplan'`, `'Immer aktiv'` |
|
||||
| `components/FlowEditor/nodes/start/ScheduleStartNodeConfig.tsx` | 22–49 | Schedule-Optionen: `'Täglich'`, `'Werktage'`, `'Bestimmte Tage'`, `'Intervall'`, `'Sekunden'`, `'Minuten'`, `'Stunden'`, `'Tage'`, `'Jahre'` |
|
||||
| `components/RbacExportImport/RbacExportImport.tsx` | 49–62 | Import-Modi: `'Zusammenführen'`, `'Nur hinzufügen'`, `'Ersetzen'` + descriptions |
|
||||
| `components/AccessRules/AccessRulesEditor.tsx` | 633–636 | Tab-Labels: `'Daten'`, `'Ressourcen'` |
|
||||
| `hooks/useAccessRules.tsx` | 23–26 | Scope-Labels: `'Keine'`, `'Eigene'`, `'Gruppe'`, `'Alle'` |
|
||||
|
||||
---
|
||||
|
||||
## 7. Action-Button `title:`-Props (in Object-Literalen)
|
||||
|
||||
| Datei | Zeilen | Titles |
|
||||
|-------|--------|--------|
|
||||
| `pages/admin/AdminUsersPage.tsx` | 200–212 | `'Bearbeiten'`, `'Löschen'`, `'Passwort-Link senden'` |
|
||||
| `pages/admin/AdminUserMandatesPage.tsx` | 352–356 | `'Rollen bearbeiten'`, `'Aus Mandant entfernen'` |
|
||||
| `pages/admin/AdminMandatesPage.tsx` | 127–234 | `'Mandant deaktivieren'`, `'Deaktivieren'`, `'Hard Delete (irreversibel)'`, `'Endgültig löschen'`, `'Bearbeiten'`, `'Deaktivieren (Soft-Delete)'` |
|
||||
| `pages/admin/AdminMandateRolesPage.tsx` | 430–435 | `'Rolle bearbeiten'`, `'Rolle löschen'` |
|
||||
| `pages/admin/AdminInvitationsPage.tsx` | 354–362 | `'Einladung widerrufen'`, `'Einladungs-Link anzeigen'` |
|
||||
| `pages/admin/AdminFeatureRolesPage.tsx` | 372–384 | `'Rolle bearbeiten'`, `'Rolle löschen'`, `'Berechtigungen verwalten'` |
|
||||
| `pages/admin/AdminFeatureInstanceUsersPage.tsx` | 535–539 | `'Rollen bearbeiten'`, `'Aus Instanz entfernen'` |
|
||||
| `pages/admin/AdminFeatureAccessPage.tsx` | 457–471 | `'Instanz löschen'`, `'Instanz bearbeiten'`, `'Rollen synchronisieren'` |
|
||||
| `pages/admin/PermissionMatrix.tsx` | 39 | `'Benutzer entfernen'` |
|
||||
| `pages/basedata/ConnectionsPage.tsx` | 324–347 | `'Bearbeiten'`, `'Löschen'`, `'Verbinden'`, `'Token erneuern'` |
|
||||
| `pages/basedata/FilesPage.tsx` | 232–458 | `'Neuer Ordner'`, `'Bearbeiten'`, `'Löschen'`, `'Herunterladen'`, `'Vorschau'` |
|
||||
| `pages/basedata/PromptsPage.tsx` | 215–225 | `'Duplizieren'`, `'Bearbeiten'`, `'Löschen'` |
|
||||
| `pages/billing/AdminSubscriptionsPage.tsx` | 67 | `'Sofort kündigen'` |
|
||||
| `pages/views/trustee/TrusteePositionsView.tsx` | 455–467 | `'Bearbeiten'`, `'Löschen'`, `'In Buchhaltung synchronisieren'` |
|
||||
| `pages/views/trustee/TrusteePositionDocumentsView.tsx` | 192–198 | `'Verknüpfung bearbeiten'`, `'Verknüpfung entfernen'` |
|
||||
| `pages/views/trustee/TrusteeDocumentsView.tsx` | 225–238 | `'Bearbeiten'`, `'Löschen'`, `'Herunterladen'` |
|
||||
| `pages/views/realestate/RealEstateProjectsView.tsx` | 167–168 | `'Bearbeiten'`, `'Löschen'` |
|
||||
| `pages/views/realestate/RealEstateParcelsView.tsx` | 186–194 | `'Bearbeiten'`, `'Löschen'` |
|
||||
| `pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx` | 136–336 | `'Workflow umbenennen'`, `'Bearbeiten'`, `'Löschen'`, `'Umbenennen'`, `'Aktivieren'`, `'Deaktivieren'`, `'Ausführen'` |
|
||||
| `pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx` | 149–289 | `'Vorlage umbenennen'`, `'Im Editor öffnen'`, `'Löschen'`, `'Umbenennen'`, `'Als Workflow kopieren'`, `'Scope ändern'` |
|
||||
| `pages/views/chatbot/ChatbotConversationsView.tsx` | 88 | `'Konversation löschen'` |
|
||||
| `components/FlowEditor/editor/Automation2FlowEditor.tsx` | 239 | `'Workflow speichern'` |
|
||||
| `components/FolderTree/FolderTree.tsx` | 384, 768 | `'Neuer Ordner'` (prompt title) |
|
||||
|
||||
---
|
||||
|
||||
## 8. Onboarding-Texte (Object-Literale)
|
||||
|
||||
| Datei | Zeilen | Labels |
|
||||
|-------|--------|--------|
|
||||
| `components/OnboardingAssistant.tsx` | 99–149 | `'Mandant einrichten'`, `'Erstes Feature aktivieren'`, `'Erste Datenquelle einbinden'`, `'Ersten AI-Chat starten'` |
|
||||
|
||||
---
|
||||
|
||||
## 9. ClickUp Node Config (Feld-Optionen)
|
||||
|
||||
| Datei | Zeilen | Labels |
|
||||
|-------|--------|--------|
|
||||
| `components/FlowEditor/nodes/configs/ClickUpNodeConfig.tsx` | 786–794 | `'Titel (name)'`, `'Beschreibung'`, `'Status'`, `'Priorität (1–4)'`, `'Fälligkeit (Datum oder ms)'`, `'Zeitschätzung (Stunden)'`, `'Zeitschätzung (ms)'`, `'Zugewiesene'`, `'Benutzerdefiniertes Feld'` |
|
||||
|
||||
---
|
||||
|
||||
## 10. Sonstige Einzel-Stellen
|
||||
|
||||
| Datei | Zeile | Property | Text |
|
||||
|-------|-------|----------|------|
|
||||
| `pages/admin/ChatbotConfigSection.tsx` | 58 | `label` | `'Althaus Preprocessor'` |
|
||||
| `pages/views/trustee/TrusteePositionsView.tsx` | 167 | `label` | `'Belege'` |
|
||||
| `pages/views/trustee/TrusteePositionsView.tsx` | 232 | `label` | `'Sync-Status'` |
|
||||
| `pages/views/trustee/TrusteePositionsView.tsx` | 445 | `label` | `'Buchhaltung synchronisieren'` |
|
||||
| `pages/views/trustee/TrusteeExpenseImportView.tsx` | 361 | `label` | `'Daily at 22:00'` |
|
||||
| `pages/basedata/PromptsPage.tsx` | 81 | `label` | `'Created By'` |
|
||||
| `pages/basedata/FilesPage.tsx` | 152 | `label` | `'Created By'` |
|
||||
| `components/FlowEditor/nodes/start/FormStartNodeConfig.tsx` | 22 | `label` | `'Feld 1'` |
|
||||
| `components/FlowEditor/nodes/start/FormStartNodeConfig.tsx` | 116 | `label` | `'Neues Feld'` |
|
||||
|
||||
---
|
||||
|
||||
## 11. Sprach-/Locale-Listen (Eigenname-Labels — evtl. NICHT übersetzen)
|
||||
|
||||
> Diese Listen enthalten Sprachnamen in der jeweiligen Sprache (Endonym). Sie werden typischerweise **nicht** übersetzt, da der User die Sprache in ihrer Originalbezeichnung erkennen soll.
|
||||
|
||||
| Datei | Zeilen | Kontext |
|
||||
|-------|--------|---------|
|
||||
| `pages/Settings.tsx` | 50–52, 563–565 | Fallback-Sprachoptionen: `'Deutsch'`, `'English'`, `'Français'` |
|
||||
| `pages/views/workspace/WorkspaceInput.tsx` | 16–27 | STT-Sprachliste (12 Sprachen) |
|
||||
| `pages/admin/AdminLanguagesPage.tsx` | 38–65 | Alle verfügbaren Sprach-Codes mit Endonymen |
|
||||
| `components/UiComponents/VoiceLanguageSelect/VoiceLanguageSelect.tsx` | 23–32 | Voice-Sprachliste mit Endonymen |
|
||||
| `components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx` | 672–675 | Fallback-Sprachoptionen |
|
||||
|
||||
---
|
||||
|
||||
## Zusammenfassung
|
||||
|
||||
| Kategorie | Anzahl Stellen | Dateien |
|
||||
|-----------|---------------|---------|
|
||||
| Hook-Defaults | 6 | 2 |
|
||||
| Monatsnamen | 24 | 2 |
|
||||
| Tab-Labels | 9 | 3 |
|
||||
| Spalten-Definitionen | ~55 | 10 |
|
||||
| Formular-Felder | ~30 | 9 |
|
||||
| Status-/Option-Maps | ~30 | 7 |
|
||||
| Action-Button titles | ~65 | 23 |
|
||||
| Onboarding-Texte | 4 | 1 |
|
||||
| ClickUp-Felder | 9 | 1 |
|
||||
| Sonstige | 9 | 5 |
|
||||
| Sprach-Listen (evtl. nicht übersetzen) | ~60 | 5 |
|
||||
| **Total (ohne Sprach-Listen)** | **~241** | **~45 Dateien** |
|
||||
17
scripts/list_t_errors.py
Normal file
17
scripts/list_t_errors.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import re
|
||||
from pathlib import Path
|
||||
|
||||
text = Path(__file__).resolve().parent.parent / "tsc-out.txt"
|
||||
content = text.read_text(encoding="utf-8")
|
||||
files = sorted(
|
||||
{
|
||||
m.group(1)
|
||||
for line in content.splitlines()
|
||||
if "Cannot find name 't'" in line
|
||||
for m in [re.match(r"^(src/[^\(:]+\.tsx)", line)]
|
||||
if m
|
||||
}
|
||||
)
|
||||
for f in files:
|
||||
print(f)
|
||||
print("TOTAL", len(files), file=__import__("sys").stderr)
|
||||
|
|
@ -5,7 +5,8 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ACCESS_LEVEL_OPTIONS, type AccessLevel, getAccessLevelColor } from '../../hooks/useAccessRules';
|
||||
import { _getAccessLevelOptions, type AccessLevel, getAccessLevelColor } from '../../hooks/useAccessRules';
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
import styles from './AccessRules.module.css';
|
||||
|
||||
interface AccessLevelSelectProps {
|
||||
|
|
@ -25,6 +26,8 @@ export const AccessLevelSelect: React.FC<AccessLevelSelectProps> = ({
|
|||
showLabel = false,
|
||||
compact = false,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const accessLevelOptions = _getAccessLevelOptions(t);
|
||||
const currentColor = getAccessLevelColor(value);
|
||||
|
||||
return (
|
||||
|
|
@ -42,7 +45,7 @@ export const AccessLevelSelect: React.FC<AccessLevelSelectProps> = ({
|
|||
color: currentColor,
|
||||
}}
|
||||
>
|
||||
{ACCESS_LEVEL_OPTIONS.map(option => (
|
||||
{accessLevelOptions.map(option => (
|
||||
<option
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ import { AccessLevelSelect } from './AccessLevelSelect';
|
|||
import { AccessRulesTable } from './AccessRulesTable';
|
||||
import styles from './AccessRules.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
|
@ -66,6 +68,7 @@ interface RuleCardProps {
|
|||
}
|
||||
|
||||
const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete }) => {
|
||||
const { t } = useLanguage();
|
||||
const isDataRule = rule.context === 'DATA';
|
||||
|
||||
return (
|
||||
|
|
@ -83,7 +86,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
|||
<button
|
||||
className={`${styles.iconButton} ${styles.danger}`}
|
||||
onClick={() => onDelete(rule.id)}
|
||||
title="Regel löschen"
|
||||
title={t('accessRulesEditor.regelLoeschen')}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
|
|
@ -137,7 +140,7 @@ const RuleCard: React.FC<RuleCardProps> = ({ rule, readOnly, onUpdate, onDelete
|
|||
/>
|
||||
</div>
|
||||
<div className={styles.permissionItem}>
|
||||
<span className={styles.permissionLabel}>Delete</span>
|
||||
<span className={styles.permissionLabel}>{t('accessRulesEditor.delete')}</span>
|
||||
<AccessLevelSelect
|
||||
value={rule.delete}
|
||||
onChange={(value) => onUpdate(rule.id, { delete: value })}
|
||||
|
|
@ -167,6 +170,7 @@ interface AddRuleFormProps {
|
|||
}
|
||||
|
||||
const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, onAdd, onCancel }) => {
|
||||
const { t } = useLanguage();
|
||||
const [item, setItem] = useState('');
|
||||
const [useCustom, setUseCustom] = useState(false);
|
||||
const [view, setView] = useState(true);
|
||||
|
|
@ -217,13 +221,13 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
|||
<form className={styles.addRuleForm} onSubmit={handleSubmit}>
|
||||
<div className={styles.formGroup}>
|
||||
<div className={styles.objectSelectorLabel}>
|
||||
<label className={styles.formLabel}>Objekt auswählen</label>
|
||||
<label className={styles.formLabel}>{t('accessRulesEditor.objektAuswaehlen')}</label>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.toggleCustomButton}
|
||||
onClick={() => setUseCustom(!useCustom)}
|
||||
>
|
||||
{useCustom ? '← Aus Katalog wählen' : 'Freie Eingabe →'}
|
||||
{useCustom ? t('accessRulesEditor.ausKatalogWaehlen') : t('accessRulesEditor.freieEingabe')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -242,7 +246,7 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
|||
onChange={(e) => setItem(e.target.value)}
|
||||
className={styles.formSelect}
|
||||
>
|
||||
<option value="">-- Global (alle Objekte) --</option>
|
||||
<option value="">{t('accessRulesEditor.globalAlleObjekte')}</option>
|
||||
{Object.entries(groupedObjects).map(([feature, objs]) => (
|
||||
<optgroup key={feature} label={feature.toUpperCase()}>
|
||||
{objs.map(obj => (
|
||||
|
|
@ -277,9 +281,9 @@ const AddRuleForm: React.FC<AddRuleFormProps> = ({ context, availableObjects, on
|
|||
{/* Header Row */}
|
||||
<div className={styles.matrixHeader}>
|
||||
<div className={styles.matrixLabel}></div>
|
||||
<div className={styles.matrixGroup}>Eigene (m)</div>
|
||||
<div className={styles.matrixGroup}>Gruppe (g)</div>
|
||||
<div className={styles.matrixGroup}>Alle (a)</div>
|
||||
<div className={styles.matrixGroup}>{t('accessRulesEditor.eigeneM')}</div>
|
||||
<div className={styles.matrixGroup}>{t('accessRulesEditor.gruppeG')}</div>
|
||||
<div className={styles.matrixGroup}>{t('accessRulesEditor.alleA')}</div>
|
||||
</div>
|
||||
|
||||
{/* CRUD Rows */}
|
||||
|
|
@ -531,6 +535,7 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
|
|||
featureCode,
|
||||
}) => {
|
||||
const { showError } = useToast();
|
||||
const { t } = useLanguage();
|
||||
const {
|
||||
rules,
|
||||
loading,
|
||||
|
|
@ -628,9 +633,9 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
|
|||
|
||||
// Render tabs
|
||||
const tabs: { id: TabType; label: string; icon: React.ReactNode; count: number }[] = [
|
||||
{ id: 'DATA', label: 'Daten', icon: <FaTable />, count: groupedRules.DATA.length },
|
||||
{ id: 'DATA', label: t('accessRulesEditor.daten'), icon: <FaTable />, count: groupedRules.DATA.length },
|
||||
{ id: 'UI', label: 'UI', icon: <FaDesktop />, count: groupedRules.UI.length },
|
||||
{ id: 'RESOURCE', label: 'Ressourcen', icon: <FaServer />, count: groupedRules.RESOURCE.length },
|
||||
{ id: 'RESOURCE', label: t('accessRulesEditor.ressourcen'), icon: <FaServer />, count: groupedRules.RESOURCE.length },
|
||||
{ id: 'JSON', label: 'JSON', icon: <FaCode />, count: rules.length },
|
||||
];
|
||||
|
||||
|
|
@ -639,7 +644,7 @@ export const AccessRulesEditor: React.FC<AccessRulesEditorProps> = ({
|
|||
<div className={styles.accessRulesEditor}>
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Berechtigungen...</span>
|
||||
<span>{t('accessRulesEditor.ladeBerechtigungen')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { FaTable, FaDesktop, FaServer, FaTrash } from 'react-icons/fa';
|
|||
import { type AccessRule, type RuleContext, type AccessLevel } from '../../hooks/useAccessRules';
|
||||
import styles from './AccessRules.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
|
@ -74,6 +76,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
|||
onUpdate,
|
||||
onDelete,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const handleLevelToggle = (
|
||||
field: 'read' | 'create' | 'update' | 'delete',
|
||||
targetLevel: 'm' | 'g' | 'a',
|
||||
|
|
@ -163,7 +166,7 @@ const AccessRuleRow: React.FC<AccessRuleRowProps> = ({
|
|||
<button
|
||||
className={`${styles.iconButton} ${styles.danger}`}
|
||||
onClick={() => onDelete(rule.id)}
|
||||
title="Regel löschen"
|
||||
title={t('accessRulesTable.regelLoeschen')}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
|
|
@ -184,6 +187,7 @@ export const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
|
|||
onUpdate,
|
||||
onDelete,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const isDataContext = context === 'DATA';
|
||||
|
||||
if (rules.length === 0) {
|
||||
|
|
@ -195,13 +199,13 @@ export const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
|
|||
<table className={styles.accessRulesTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={styles.colObject}>Objekt (Dot-Notation)</th>
|
||||
<th className={styles.colObject}>{t('accessRulesTable.objektDotnotation')}</th>
|
||||
<th className={styles.colView}>View</th>
|
||||
{isDataContext && (
|
||||
<>
|
||||
<th className={styles.colGroupHeader} colSpan={4}>Eigene (m)</th>
|
||||
<th className={styles.colGroupHeader} colSpan={4}>Gruppe (g)</th>
|
||||
<th className={styles.colGroupHeader} colSpan={4}>Alle (a)</th>
|
||||
<th className={styles.colGroupHeader} colSpan={4}>{t('accessRulesTable.eigeneM')}</th>
|
||||
<th className={styles.colGroupHeader} colSpan={4}>{t('accessRulesTable.gruppeG')}</th>
|
||||
<th className={styles.colGroupHeader} colSpan={4}>{t('accessRulesTable.alleA')}</th>
|
||||
</>
|
||||
)}
|
||||
<th className={styles.colActions}></th>
|
||||
|
|
@ -213,15 +217,15 @@ export const AccessRulesTable: React.FC<AccessRulesTableProps> = ({
|
|||
<th title="Create">C</th>
|
||||
<th title="Read">R</th>
|
||||
<th title="Update">U</th>
|
||||
<th title="Delete">D</th>
|
||||
<th title={t('accessRulesTable.delete')}>D</th>
|
||||
<th title="Create">C</th>
|
||||
<th title="Read">R</th>
|
||||
<th title="Update">U</th>
|
||||
<th title="Delete">D</th>
|
||||
<th title={t('accessRulesTable.delete')}>D</th>
|
||||
<th title="Create">C</th>
|
||||
<th title="Read">R</th>
|
||||
<th title="Update">U</th>
|
||||
<th title="Delete">D</th>
|
||||
<th title={t('accessRulesTable.delete')}>D</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -61,11 +61,11 @@ export function ContentPreview({
|
|||
if (isOpen && fileId) {
|
||||
// Check if we have valid data
|
||||
if (!fileId || fileId === 'undefined' || fileId === 'null') {
|
||||
setError('Invalid file ID');
|
||||
setError(t('contentPreview.invalidFileId'));
|
||||
return;
|
||||
}
|
||||
if (!fileName || fileName === 'Unknown Item') {
|
||||
setError('File name not available');
|
||||
setError(t('contentPreview.fileNameNotAvailable'));
|
||||
return;
|
||||
}
|
||||
loadPreview();
|
||||
|
|
@ -98,7 +98,7 @@ export function ContentPreview({
|
|||
setError(result.error || 'Failed to load preview');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('An unexpected error occurred while loading the preview');
|
||||
setError(t('contentPreview.anUnexpectedErrorOccurredWhile'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ export function ContentPreview({
|
|||
previewUrl={undefined}
|
||||
previewContent={previewContent}
|
||||
fileName={fileName}
|
||||
onError={() => setError('Failed to load PDF preview')}
|
||||
onError={() => setError(t('contentPreview.failedToLoadPdfPreview'))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -194,9 +194,9 @@ export function ContentPreview({
|
|||
return (
|
||||
<div className={styles.jsonContainer}>
|
||||
<div className={styles.jsonHeader}>
|
||||
<span className={styles.jsonTitle}>JSON Preview (Fallback)</span>
|
||||
<span className={styles.jsonTitle}>{t('contentPreview.jsonPreviewFallback')}</span>
|
||||
<div className={styles.jsonHeaderRight}>
|
||||
<span className={styles.jsonSize}>Raw content</span>
|
||||
<span className={styles.jsonSize}>{t('contentPreview.rawContent')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<pre className={styles.jsonPreview}>
|
||||
|
|
@ -219,7 +219,7 @@ export function ContentPreview({
|
|||
<ImageRenderer
|
||||
previewUrl={previewUrl}
|
||||
fileName={fileName}
|
||||
onError={() => setError('Failed to load image preview')}
|
||||
onError={() => setError(t('contentPreview.failedToLoadImagePreview'))}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ export function ContentPreview({
|
|||
<HtmlRenderer
|
||||
previewUrl={previewUrl}
|
||||
fileName={fileName}
|
||||
onError={() => setError('Failed to load HTML preview')}
|
||||
onError={() => setError(t('contentPreview.failedToLoadHtmlPreview'))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -240,7 +240,7 @@ export function ContentPreview({
|
|||
previewUrl={previewUrl}
|
||||
fileName={fileName}
|
||||
mimeType={mimeType}
|
||||
onError={() => setError('Failed to load text preview')}
|
||||
onError={() => setError(t('contentPreview.failedToLoadTextPreview'))}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ export function ContentPreview({
|
|||
previewUrl={previewUrl}
|
||||
previewContent={previewContent || undefined}
|
||||
fileName={fileName}
|
||||
onError={() => setError('Failed to load PDF preview')}
|
||||
onError={() => setError(t('contentPreview.failedToLoadPdfPreview'))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -267,7 +267,7 @@ export function ContentPreview({
|
|||
<HtmlRenderer
|
||||
previewUrl={previewUrl}
|
||||
fileName={fileName}
|
||||
onError={() => setError('Failed to load HTML preview')}
|
||||
onError={() => setError(t('contentPreview.failedToLoadHtmlPreview'))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -279,7 +279,7 @@ export function ContentPreview({
|
|||
previewUrl={previewUrl}
|
||||
fileName={fileName}
|
||||
mimeType={mimeType}
|
||||
onError={() => setError('Preview not supported for this file type')}
|
||||
onError={() => setError(t('contentPreview.previewNotSupportedForThis'))}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export function UrlContentPreview({
|
|||
}
|
||||
// If PDF.js also fails, show error
|
||||
setIsLoading(false);
|
||||
setError('Failed to load PDF. This might be due to CORS restrictions. You can try downloading the file or opening it in a new tab.');
|
||||
setError(t('urlContentPreview.failedToLoadPdfThis'));
|
||||
setShowPdfAnyway(true);
|
||||
};
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ export function UrlContentPreview({
|
|||
} else if (isLoading && !hasLoaded && usePdfJs) {
|
||||
// PDF.js also failed, show error
|
||||
setShowPdfAnyway(true);
|
||||
setError('PDF lädt langsam. Bitte verwenden Sie den Download-Button oder öffnen Sie es in einem neuen Tab.');
|
||||
setError(t('urlContentPreview.pdfLaedtLangsamBitteVerwenden'));
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, QUICK_TIMEOUT);
|
||||
|
|
@ -129,7 +129,7 @@ export function UrlContentPreview({
|
|||
try {
|
||||
new URL(url);
|
||||
} catch (e) {
|
||||
setError('Invalid URL');
|
||||
setError(t('urlContentPreview.invalidUrl'));
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -314,7 +314,7 @@ export function UrlContentPreview({
|
|||
<div className={styles.unsupportedContainer}>
|
||||
<div className={styles.unsupportedIcon}>📄</div>
|
||||
<div className={styles.fileName}>{fileName}</div>
|
||||
<p>Preview not supported for this file type. Please download the file to view it.</p>
|
||||
<p>{t('urlContentPreview.previewNotSupportedForThis')}</p>
|
||||
<button onClick={handleDownload} className={styles.retryButton}>
|
||||
Download File
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@ export function JsonRenderer({ previewContent, fileName }: JsonRendererProps) {
|
|||
) : (
|
||||
typeof data.values[index] === 'object' && data.values[index] !== null && 'keys' in data.values[index] ?
|
||||
renderTable(data.values[index], level + 1, rowPath) :
|
||||
<span className={styles.jsonValue}>Error: Invalid nested data</span>
|
||||
<span className={styles.jsonValue}>{t('jsonRenderer.errorInvalidNestedData')}</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { useEffect, useRef, useState } from 'react';
|
|||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import styles from '../ContentPreview.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
// Set worker source for PDF.js
|
||||
if (typeof window !== 'undefined') {
|
||||
// Try to use local worker first, fallback to CDN
|
||||
|
|
@ -24,7 +26,9 @@ interface PdfJsRendererProps {
|
|||
onLoad?: () => void;
|
||||
}
|
||||
|
||||
export function PdfJsRenderer({ previewUrl, fileName: _fileName, onError, onLoad }: PdfJsRendererProps) {
|
||||
export function PdfJsRenderer({
|
||||
previewUrl, fileName: _fileName, onError, onLoad }: PdfJsRendererProps) {
|
||||
const { t } = useLanguage();
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
|
@ -137,7 +141,7 @@ export function PdfJsRenderer({ previewUrl, fileName: _fileName, onError, onLoad
|
|||
return (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner}></div>
|
||||
<p>PDF wird geladen...</p>
|
||||
<p>{t('pdfJsRenderer.pdfWirdGeladen')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import styles from '../ContentPreview.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
// Updated to handle both previewUrl and previewContent
|
||||
|
||||
interface TextRendererProps {
|
||||
|
|
@ -10,13 +12,15 @@ interface TextRendererProps {
|
|||
onError: () => void;
|
||||
}
|
||||
|
||||
export function TextRenderer({ previewUrl, previewContent, fileName, mimeType, onError }: TextRendererProps) {
|
||||
export function TextRenderer({
|
||||
previewUrl, previewContent, fileName, mimeType, onError }: TextRendererProps) {
|
||||
const { t } = useLanguage();
|
||||
// If we have previewContent directly, display it as text
|
||||
if (previewContent && !previewUrl) {
|
||||
return (
|
||||
<div className={styles.textContainer}>
|
||||
<div className={styles.textHeader}>
|
||||
<span className={styles.textTitle}>Text Preview</span>
|
||||
<span className={styles.textTitle}>{t('textRenderer.textPreview')}</span>
|
||||
</div>
|
||||
<pre className={styles.textPreview}>
|
||||
<code className={styles.textCode}>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ import { UnifiedDataBar } from '../../../components/UnifiedDataBar';
|
|||
import type { UdbContext, UdbTab } from '../../../components/UnifiedDataBar';
|
||||
import styles from './Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
const LOG = '[Automation2]';
|
||||
|
||||
const DEFAULT_INVOCATIONS = (): WorkflowEntryPoint[] =>
|
||||
|
|
@ -72,8 +74,7 @@ interface Automation2FlowEditorProps {
|
|||
onSourcesChanged?: () => void;
|
||||
}
|
||||
|
||||
export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
|
||||
instanceId,
|
||||
export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ instanceId,
|
||||
mandateId,
|
||||
language = 'de',
|
||||
initialWorkflowId,
|
||||
|
|
@ -84,6 +85,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
|
|||
onFileSelect,
|
||||
onSourcesChanged,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { request } = useApiRequest();
|
||||
const { prompt: promptInput, PromptDialog } = usePrompt();
|
||||
const [nodeTypes, setNodeTypes] = useState<NodeType[]>([]);
|
||||
|
|
@ -235,7 +237,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
|
|||
setExecuteResult({ success: true } as ExecuteGraphResponse);
|
||||
} else {
|
||||
const label = await promptInput('Workflow-Name:', {
|
||||
title: 'Workflow speichern',
|
||||
title: t('automation2FlowEditor.saveWorkflow'),
|
||||
defaultValue: 'Neuer Workflow',
|
||||
placeholder: 'Name des Workflows',
|
||||
});
|
||||
|
|
@ -593,7 +595,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({
|
|||
</div>
|
||||
<div className={styles.loading}>
|
||||
<FaSpinner className={styles.spinner} style={{ marginBottom: '0.5rem' }} />
|
||||
<p>Lade Node-Typen...</p>
|
||||
<p>{t('automation2FlowEditor.ladeNodetypen')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { FaCog, FaPlay, FaSpinner, FaCloudUploadAlt, FaCloudDownloadAlt, FaArchi
|
|||
import type { Automation2Workflow, ExecuteGraphResponse, AutoVersion, AutoTemplateScope } from '../../../api/workflowApi';
|
||||
import styles from './Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
interface CanvasHeaderProps {
|
||||
workflows: Automation2Workflow[];
|
||||
currentWorkflowId: string | null;
|
||||
|
|
@ -34,14 +36,15 @@ interface CanvasHeaderProps {
|
|||
onWorkflowRename?: (workflowId: string, newName: string) => void;
|
||||
}
|
||||
|
||||
const STATUS_BADGE: Record<string, { label: string; color: string }> = {
|
||||
draft: { label: 'Entwurf', color: 'var(--warning-color, #ffc107)' },
|
||||
published: { label: 'Veröffentlicht', color: 'var(--success-color, #28a745)' },
|
||||
archived: { label: 'Archiviert', color: 'var(--text-secondary, #666)' },
|
||||
function _getStatusBadge(t: (key: string) => string): Record<string, { label: string; color: string }> {
|
||||
return {
|
||||
draft: { label: t('canvasHeader.entwurf'), color: 'var(--warning-color, #ffc107)' },
|
||||
published: { label: t('canvasHeader.veroeffentlicht'), color: 'var(--success-color, #28a745)' },
|
||||
archived: { label: t('canvasHeader.archiviert'), color: 'var(--text-secondary, #666)' },
|
||||
};
|
||||
}
|
||||
|
||||
export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
||||
workflows,
|
||||
export const CanvasHeader: React.FC<CanvasHeaderProps> = ({ workflows,
|
||||
currentWorkflowId,
|
||||
onWorkflowSelect,
|
||||
onNew,
|
||||
|
|
@ -66,9 +69,11 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
|||
onNewFromTemplate,
|
||||
onWorkflowRename,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const statusBadge = _getStatusBadge(t);
|
||||
const currentVersion = versions?.find((v) => v.id === currentVersionId);
|
||||
const currentStatus = currentVersion?.status || 'draft';
|
||||
const badge = STATUS_BADGE[currentStatus] || STATUS_BADGE.draft;
|
||||
const badge = statusBadge[currentStatus] || statusBadge.draft;
|
||||
|
||||
const [newMenuOpen, setNewMenuOpen] = useState(false);
|
||||
const newMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -148,7 +153,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
|||
<button
|
||||
type="button"
|
||||
className={styles.canvasGearBtn}
|
||||
title="Workflow-Konfiguration (Einstieg / Starts)"
|
||||
title={t('canvasHeader.workflowkonfigurationEinstiegStarts')}
|
||||
aria-label="Workflow-Konfiguration"
|
||||
onClick={onWorkflowSettings}
|
||||
>
|
||||
|
|
@ -167,7 +172,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
|||
className={styles.retryButton}
|
||||
onClick={() => setNewMenuOpen((p) => !p)}
|
||||
style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0, paddingLeft: 4, paddingRight: 6, borderLeft: '1px solid rgba(0,0,0,0.15)' }}
|
||||
title="Neu aus Vorlage"
|
||||
title={t('canvasHeader.neuAusVorlage')}
|
||||
>
|
||||
<FaCaretDown style={{ fontSize: '0.7rem' }} />
|
||||
</button>
|
||||
|
|
@ -211,9 +216,9 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
|||
className={styles.retryButton}
|
||||
onClick={() => setTemplateMenuOpen((p) => !p)}
|
||||
disabled={templateSaving}
|
||||
title="Als Vorlage speichern"
|
||||
title={t('canvasHeader.alsVorlageSpeichern')}
|
||||
>
|
||||
{templateSaving ? <FaSpinner className={styles.spinner} /> : <><FaBookmark style={{ marginRight: 4 }} />Als Vorlage</>}
|
||||
{templateSaving ? <FaSpinner className={styles.spinner} /> : <><FaBookmark style={{ marginRight: 4 }} />{t('canvasHeader.alsVorlage')}</>}
|
||||
</button>
|
||||
{templateMenuOpen && (
|
||||
<div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 100, background: 'var(--bg-primary, #fff)', border: '1px solid var(--border-color, #e0e0e0)', borderRadius: 6, boxShadow: '0 4px 12px rgba(0,0,0,0.15)', minWidth: 180, marginTop: 4 }}>
|
||||
|
|
@ -239,7 +244,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
|||
}}
|
||||
style={{ padding: '0.4rem', minWidth: 180 }}
|
||||
>
|
||||
<option value="">— Workflow laden —</option>
|
||||
<option value="">{t('canvasHeader.workflowLaden')}</option>
|
||||
{workflows.map((w) => (
|
||||
<option key={w.id} value={w.id}>
|
||||
{w.label}
|
||||
|
|
@ -265,7 +270,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
|||
)}
|
||||
</button>
|
||||
{onToggleChat && (
|
||||
<button type="button" className={styles.retryButton} onClick={onToggleChat} title="Workspace-Panel (Chats, Dateien, Quellen)">
|
||||
<button type="button" className={styles.retryButton} onClick={onToggleChat} title={t('canvasHeader.workspacepanelChatsDateienQuellen')}>
|
||||
<FaDatabase style={{ marginRight: '0.4rem' }} />
|
||||
Workspace
|
||||
</button>
|
||||
|
|
@ -282,10 +287,10 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
|||
style={{ padding: '0.3rem', minWidth: 140, fontSize: '0.85rem' }}
|
||||
disabled={versionLoading}
|
||||
>
|
||||
<option value="">— Aktuelle —</option>
|
||||
<option value="">{t('canvasHeader.aktuelle')}</option>
|
||||
{versions.map((v) => (
|
||||
<option key={v.id} value={v.id}>
|
||||
v{v.versionNumber} ({STATUS_BADGE[v.status]?.label ?? v.status})
|
||||
v{v.versionNumber} ({statusBadge[v.status]?.label ?? v.status})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -307,7 +312,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
|||
className={styles.retryButton}
|
||||
onClick={() => onPublishVersion(currentVersion.id)}
|
||||
disabled={versionLoading}
|
||||
title="Version veröffentlichen"
|
||||
title={t('canvasHeader.versionVeroeffentlichen')}
|
||||
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
|
||||
>
|
||||
<FaCloudUploadAlt style={{ marginRight: 4 }} />
|
||||
|
|
@ -320,7 +325,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
|||
className={styles.retryButton}
|
||||
onClick={() => onUnpublishVersion(currentVersion.id)}
|
||||
disabled={versionLoading}
|
||||
title="Veröffentlichung zurücknehmen"
|
||||
title={t('canvasHeader.veroeffentlichungZuruecknehmen')}
|
||||
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
|
||||
>
|
||||
<FaCloudDownloadAlt style={{ marginRight: 4 }} />
|
||||
|
|
@ -333,7 +338,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
|||
className={styles.retryButton}
|
||||
onClick={() => onArchiveVersion(currentVersion.id)}
|
||||
disabled={versionLoading}
|
||||
title="Version archivieren"
|
||||
title={t('canvasHeader.versionArchivieren')}
|
||||
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
|
||||
>
|
||||
<FaArchive style={{ marginRight: 4 }} />
|
||||
|
|
@ -346,7 +351,7 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
|||
className={styles.retryButton}
|
||||
onClick={onCreateDraft}
|
||||
disabled={versionLoading}
|
||||
title="Neuen Entwurf erstellen"
|
||||
title={t('canvasHeader.neuenEntwurfErstellen')}
|
||||
style={{ fontSize: '0.8rem', padding: '0.25rem 0.6rem' }}
|
||||
>
|
||||
+ Entwurf
|
||||
|
|
@ -376,10 +381,10 @@ export const CanvasHeader: React.FC<CanvasHeaderProps> = ({
|
|||
}}
|
||||
>
|
||||
{executeResult.success ? (
|
||||
<>✓ Ausführung abgeschlossen.</>
|
||||
<>{t('canvasHeader.ausfuehrungAbgeschlossen')}</>
|
||||
) : (executeResult as { paused?: boolean }).paused ? (
|
||||
<>
|
||||
⏸ Workflow pausiert. Öffne <strong>Workflows & Tasks</strong> in der Sidebar, um den
|
||||
⏸ Workflow pausiert. Öffne <strong>{t('canvasHeader.workflowsTasks')}</strong> in der Sidebar, um den
|
||||
Task zu bearbeiten.
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { ChatMessageList } from '../../Chat';
|
|||
import type { ChatMessage } from '../../Chat';
|
||||
import { getPageIcon } from '../../../config/pageRegistry';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
export interface PendingFile {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
|
|
@ -46,8 +48,7 @@ interface EditorChatPanelProps {
|
|||
|
||||
let _msgCounter = 0;
|
||||
|
||||
export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({
|
||||
instanceId,
|
||||
export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({ instanceId,
|
||||
workflowId,
|
||||
onGraphUpdated,
|
||||
pendingFiles = [],
|
||||
|
|
@ -55,6 +56,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({
|
|||
dataSources = [],
|
||||
featureDataSources = [],
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [prompt, setPrompt] = useState('');
|
||||
|
|
@ -187,7 +189,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({
|
|||
<ChatMessageList
|
||||
messages={messages}
|
||||
isProcessing={loading}
|
||||
emptyMessage="Describe what you want to build. The AI will create and modify nodes on the canvas."
|
||||
emptyMessage={t('editorChatPanel.describeWhatYouWantTo')}
|
||||
/>
|
||||
|
||||
{/* Pending files (from UDB drag/click) */}
|
||||
|
|
@ -277,7 +279,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({
|
|||
value={prompt}
|
||||
onChange={e => setPrompt(e.target.value)}
|
||||
onKeyDown={_handleKeyDown}
|
||||
placeholder={workflowId ? 'Describe a change...' : 'Save workflow first'}
|
||||
placeholder={workflowId ? t('editorChatPanel.describeAChange') : t('editorChatPanel.saveWorkflowFirst')}
|
||||
disabled={!workflowId || loading}
|
||||
style={{
|
||||
flex: 1, minHeight: 36, maxHeight: 100, resize: 'vertical',
|
||||
|
|
@ -294,7 +296,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({
|
|||
<button
|
||||
onClick={() => setShowSourcePicker(prev => !prev)}
|
||||
disabled={loading || !workflowId}
|
||||
title="Datenquellen anhängen"
|
||||
title={t('editorChatPanel.datenquellenAnhaengen')}
|
||||
style={{
|
||||
width: 36, height: 36, borderRadius: 8,
|
||||
border: '1px solid var(--border-color, #ddd)',
|
||||
|
|
@ -399,7 +401,7 @@ export const EditorChatPanel: React.FC<EditorChatPanelProps> = ({
|
|||
background: prompt.trim() && workflowId ? 'var(--primary-color, #F25843)' : '#ccc',
|
||||
color: '#fff', cursor: prompt.trim() && workflowId ? 'pointer' : 'default',
|
||||
fontWeight: 600, fontSize: 12,
|
||||
}}>Send</button>
|
||||
}}>{t('editorChatPanel.send')}</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||
import type { NodeType } from '../../../api/workflowApi';
|
||||
import styles from './Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
export interface CanvasNode {
|
||||
id: string;
|
||||
type: string;
|
||||
|
|
@ -78,8 +80,7 @@ const HIGHLIGHT_COLORS: Record<string, string> = {
|
|||
skipped: '#6c757d',
|
||||
};
|
||||
|
||||
export const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
||||
nodes,
|
||||
export const FlowCanvas: React.FC<FlowCanvasProps> = ({ nodes,
|
||||
connections,
|
||||
nodeTypes,
|
||||
onNodesChange,
|
||||
|
|
@ -90,6 +91,7 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|||
onSelectionChange,
|
||||
highlightedNodeIds,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [selectedNodeIds, setSelectedNodeIds] = useState<Set<string>>(new Set());
|
||||
const selectedNodeId = selectedNodeIds.size === 1 ? [...selectedNodeIds][0] : null;
|
||||
|
|
@ -638,7 +640,7 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|||
style={{ cursor: 'pointer' }}
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
aria-label="Verbindung auswählen (Entf zum Löschen, klicken Sie auf einen anderen Eingang zum Umleiten)"
|
||||
aria-label={t('flowCanvas.verbindungAuswaehlenEntfZumLoeschen')}
|
||||
>
|
||||
<path
|
||||
d={pathD}
|
||||
|
|
@ -657,7 +659,7 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|||
pointerEvents="none"
|
||||
/>
|
||||
{isWarning && !isSelected && (
|
||||
<title>Type mismatch warning: output type may not match input type</title>
|
||||
<title>{t('flowCanvas.typeMismatchWarningOutputType')}</title>
|
||||
)}
|
||||
</g>
|
||||
);
|
||||
|
|
@ -756,8 +758,8 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|||
outputLabel ??
|
||||
(selectedConnectionId && !isOutput
|
||||
? used
|
||||
? 'Aktuelles Ziel – klicken zum Abwählen'
|
||||
: 'Klicken zum Umleiten'
|
||||
? t('flowCanvas.aktuellesZielKlickenZumAbwaehlen')
|
||||
: t('flowCanvas.klickenZumUmleiten')
|
||||
: undefined)
|
||||
}
|
||||
/>
|
||||
|
|
@ -836,7 +838,7 @@ export const FlowCanvas: React.FC<FlowCanvasProps> = ({
|
|||
)}
|
||||
{nodes.length === 0 && (
|
||||
<div className={styles.canvasPlaceholder}>
|
||||
<p>Nodes aus der Liste links hierher ziehen.</p>
|
||||
<p>{t('flowCanvas.nodesAusDerListeLinks')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { getLabel } from '../nodes/shared/utils';
|
|||
import { FRONTEND_TYPE_RENDERERS } from '../nodes/frontendTypeRenderers';
|
||||
import styles from './Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
interface NodeConfigPanelProps {
|
||||
node: CanvasNode | null;
|
||||
nodeType: NodeType | undefined;
|
||||
|
|
@ -22,16 +24,16 @@ interface NodeConfigPanelProps {
|
|||
request?: ApiRequestFunction;
|
||||
}
|
||||
|
||||
export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({
|
||||
node,
|
||||
export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({ node,
|
||||
nodeType,
|
||||
language,
|
||||
onParametersChange,
|
||||
onMergeNodeParameters,
|
||||
onMergeNodeParameters: _onMergeNodeParameters,
|
||||
onNodeUpdate,
|
||||
instanceId,
|
||||
request,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [params, setParams] = useState<Record<string, unknown>>({});
|
||||
const nodeIdRef = useRef<string | undefined>(undefined);
|
||||
nodeIdRef.current = node?.id;
|
||||
|
|
@ -80,13 +82,13 @@ export const NodeConfigPanel: React.FC<NodeConfigPanelProps> = ({
|
|||
<div className={styles.nodeConfigPanel}>
|
||||
{showNameField && (
|
||||
<div className={styles.nodeConfigNameRow}>
|
||||
<label htmlFor="node-config-name">Bezeichnung</label>
|
||||
<label htmlFor="node-config-name">{t('nodeConfigPanel.bezeichnung')}</label>
|
||||
<input
|
||||
id="node-config-name"
|
||||
type="text"
|
||||
value={node.title ?? ''}
|
||||
onChange={(e) => onNodeUpdate(node.id, { title: e.target.value })}
|
||||
placeholder="z.B. Kundenformular, Prüfen Land"
|
||||
placeholder={t('nodeConfigPanel.zbKundenformularPruefenLand')}
|
||||
/>
|
||||
<p className={styles.nodeConfigNameHint}>
|
||||
Wird im Data Picker angezeigt, um diesen Node zu identifizieren.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { getLabel } from '../nodes/shared/utils';
|
|||
import { NodeListItem } from './NodeListItem';
|
||||
import styles from './Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
interface NodeSidebarProps {
|
||||
nodeTypes: NodeType[];
|
||||
categories: NodeTypeCategory[];
|
||||
|
|
@ -24,8 +26,7 @@ interface NodeSidebarProps {
|
|||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const NodeSidebar: React.FC<NodeSidebarProps> = ({
|
||||
nodeTypes,
|
||||
export const NodeSidebar: React.FC<NodeSidebarProps> = ({ nodeTypes,
|
||||
categories,
|
||||
filter,
|
||||
onFilterChange,
|
||||
|
|
@ -35,6 +36,7 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({
|
|||
excludedCategories,
|
||||
style,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const filteredNodeTypes = useMemo(() => {
|
||||
const visible = nodeTypes.filter(
|
||||
(n) =>
|
||||
|
|
@ -76,8 +78,8 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({
|
|||
return result;
|
||||
}, [groupedByCategory]);
|
||||
|
||||
const getLabelFn = (t: string | Record<string, string> | undefined, lang?: string) =>
|
||||
getLabel(t, lang ?? language);
|
||||
const getLabelFn = (multilingual: string | Record<string, string> | undefined, lang?: string) =>
|
||||
getLabel(multilingual, lang ?? language);
|
||||
|
||||
return (
|
||||
<div className={styles.sidebar} style={style}>
|
||||
|
|
@ -86,7 +88,7 @@ export const NodeSidebar: React.FC<NodeSidebarProps> = ({
|
|||
<input
|
||||
type="text"
|
||||
className={styles.sidebarSearch}
|
||||
placeholder="Nodes durchsuchen..."
|
||||
placeholder={t('nodeSidebar.nodesDurchsuchen')}
|
||||
value={filter}
|
||||
onChange={(e) => onFilterChange(e.target.value)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import React, { useState, useEffect, useCallback, useRef } from 'react';
|
|||
import { useApiRequest } from '../../../hooks/useApi';
|
||||
import type { AutoStepLog } from '../../../api/workflowApi';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
interface RunTracingPanelProps {
|
||||
instanceId: string;
|
||||
runId: string | null;
|
||||
|
|
@ -49,7 +51,9 @@ function _truncateJson(obj: unknown, maxLen = 300): string {
|
|||
}
|
||||
}
|
||||
|
||||
const CollapsibleSection: React.FC<{ label: string; content: string }> = ({ label, content }) => {
|
||||
const CollapsibleSection: React.FC<{
|
||||
label: string; content: string;
|
||||
}> = ({ label, content }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
if (!content) return null;
|
||||
return (
|
||||
|
|
@ -82,6 +86,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
|||
onNodeSelect,
|
||||
onActiveStepsChange,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [steps, setSteps] = useState<AutoStepLog[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sseConnected, setSseConnected] = useState(false);
|
||||
|
|
@ -178,7 +183,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
|||
Run Steps {loading && <span style={{ fontWeight: 400, fontSize: '12px', color: '#888' }}>(loading...)</span>}
|
||||
</div>
|
||||
{steps.length === 0 && !loading && (
|
||||
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px' }}>No steps recorded yet.</div>
|
||||
<div style={{ color: 'var(--text-secondary, #888)', fontSize: '13px' }}>{t('runTracingPanel.noStepsRecordedYet')}</div>
|
||||
)}
|
||||
{steps.map((step: any) => {
|
||||
const startStr = _formatTimestamp(step.startedAt);
|
||||
|
|
@ -217,7 +222,7 @@ export const RunTracingPanel: React.FC<RunTracingPanelProps> = ({
|
|||
</span>
|
||||
<span style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
||||
{step.retryCount > 0 && (
|
||||
<span style={{ color: '#f0ad4e', fontSize: '11px' }} title="Retry count">
|
||||
<span style={{ color: '#f0ad4e', fontSize: '11px' }} title={t('runTracingPanel.retryCount')}>
|
||||
{step.retryCount}x retry
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,17 @@ import {
|
|||
} from '../nodes/runtime/workflowStartSync';
|
||||
import styles from './Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
/** Vier Einstiege; bei „Immer aktiv“ folgt später die Listener-Konfiguration (E-Mail, Webhook, …). */
|
||||
const KIND_OPTIONS: { value: string; label: string }[] = [
|
||||
{ value: 'manual', label: 'Manueller Trigger' },
|
||||
{ value: 'form', label: 'Formular' },
|
||||
{ value: 'schedule', label: 'Zeitplan' },
|
||||
{ value: 'always_on', label: 'Immer aktiv' },
|
||||
function _getKindOptions(t: (key: string) => string): { value: string; label: string }[] {
|
||||
return [
|
||||
{ value: 'manual', label: t('workflowConfigurationModal.manuellerTrigger') },
|
||||
{ value: 'form', label: t('workflowConfigurationModal.formular') },
|
||||
{ value: 'schedule', label: t('workflowConfigurationModal.zeitplan') },
|
||||
{ value: 'always_on', label: t('workflowConfigurationModal.immerAktiv') },
|
||||
];
|
||||
}
|
||||
|
||||
interface WorkflowConfigurationModalProps {
|
||||
open: boolean;
|
||||
|
|
@ -25,19 +29,22 @@ interface WorkflowConfigurationModalProps {
|
|||
onApply: (next: WorkflowEntryPoint[]) => void;
|
||||
}
|
||||
|
||||
const _validKinds = ['manual', 'form', 'schedule', 'always_on'];
|
||||
|
||||
function normalizeLoadedKind(k: string): string {
|
||||
if (KIND_OPTIONS.some((o) => o.value === k)) return k;
|
||||
if (_validKinds.includes(k)) return k;
|
||||
if (['email', 'webhook', 'event'].includes(k)) return 'always_on';
|
||||
if (k === 'api') return 'manual';
|
||||
return 'manual';
|
||||
}
|
||||
|
||||
export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProps> = ({
|
||||
open,
|
||||
export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProps> = ({ open,
|
||||
onClose,
|
||||
invocations,
|
||||
onApply,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const kindOptions = _getKindOptions(t);
|
||||
const [kind, setKind] = useState(() => normalizeLoadedKind(getPrimaryStartKind(invocations)));
|
||||
const [titleDe, setTitleDe] = useState('');
|
||||
|
||||
|
|
@ -46,9 +53,9 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
|
|||
const k = normalizeLoadedKind(getPrimaryStartKind(invocations));
|
||||
setKind(k);
|
||||
const entry = invocations[0];
|
||||
const t = entry?.title;
|
||||
if (typeof t === 'string') setTitleDe(t);
|
||||
else if (t && typeof t === 'object') setTitleDe(t.de || t.en || '');
|
||||
const entryTitle = entry?.title;
|
||||
if (typeof entryTitle === 'string') setTitleDe(entryTitle);
|
||||
else if (entryTitle && typeof entryTitle === 'object') setTitleDe(entryTitle.de || entryTitle.en || '');
|
||||
else setTitleDe('');
|
||||
}, [open, invocations]);
|
||||
|
||||
|
|
@ -57,7 +64,7 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
|
|||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const label =
|
||||
titleDe.trim() || KIND_OPTIONS.find((o) => o.value === kind)?.label || 'Start';
|
||||
titleDe.trim() || kindOptions.find((o) => o.value === kind)?.label || 'Start';
|
||||
const next = buildInvocationsForPrimaryKind(kind, invocations, label);
|
||||
onApply(next);
|
||||
onClose();
|
||||
|
|
@ -82,11 +89,11 @@ export const WorkflowConfigurationModal: React.FC<WorkflowConfigurationModalProp
|
|||
className={styles.workflowModalInput}
|
||||
value={titleDe}
|
||||
onChange={(e) => setTitleDe(e.target.value)}
|
||||
placeholder="z. B. Angebot anlegen"
|
||||
placeholder={t('workflowConfigurationModal.zBAngebotAnlegen')}
|
||||
/>
|
||||
|
||||
<div className={styles.workflowModalRadioGroup} role="radiogroup" aria-label="Einstiegsart">
|
||||
{KIND_OPTIONS.map((o) => (
|
||||
{kindOptions.map((o) => (
|
||||
<label key={o.value} className={styles.workflowModalRadio}>
|
||||
<input
|
||||
type="radio"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@
|
|||
import React from 'react';
|
||||
import type { NodeConfigRendererProps } from './types';
|
||||
|
||||
export const ApprovalNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => (
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export const ApprovalNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label>Titel</label>
|
||||
|
|
@ -16,12 +20,13 @@ export const ApprovalNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Beschreibung</label>
|
||||
<label>{t('approvalNodeConfig.beschreibung')}</label>
|
||||
<textarea
|
||||
value={(params.description as string) ?? ''}
|
||||
onChange={(e) => updateParam('description', e.target.value)}
|
||||
placeholder="Was genehmigt werden soll"
|
||||
placeholder={t('approvalNodeConfig.wasGenehmigtWerdenSoll')}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import {
|
|||
} from '../shared/clickupFormSync';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
/** ClickUp browse: list folder paths end with /list/{id} */
|
||||
const LIST_SEGMENT_RE = /\/list\/([^/]+)$/;
|
||||
|
||||
|
|
@ -352,6 +354,7 @@ function ClickUpListRelationshipFieldRow({
|
|||
value: unknown;
|
||||
onChange: (v: unknown) => void;
|
||||
}) {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
const [tasks, setTasks] = useState<Array<{ id: string; name: string }>>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -399,7 +402,7 @@ function ClickUpListRelationshipFieldRow({
|
|||
<StatischKontextSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="— Quelle wählen —"
|
||||
placeholder={t('clickUpNodeConfig.quelleWaehlen')}
|
||||
staticLabel="Statisch (Aufgabe wählen)"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -419,7 +422,7 @@ function ClickUpListRelationshipFieldRow({
|
|||
else onChange(createValue({ add: [tid], rem: [] }));
|
||||
}}
|
||||
>
|
||||
<option value="">— Aufgabe wählen —</option>
|
||||
<option value="">{t('clickUpNodeConfig.aufgabeWaehlen')}</option>
|
||||
{tasks.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.name}
|
||||
|
|
@ -472,6 +475,7 @@ function ClickUpCustomFieldRow({
|
|||
/** List where the new task is created — fallback if relationship omits linked list (same-list). */
|
||||
parentListId?: string;
|
||||
}) {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
const fid = String(field.id ?? '');
|
||||
const fname = String(field.name ?? fid);
|
||||
|
|
@ -539,7 +543,7 @@ function ClickUpCustomFieldRow({
|
|||
<StatischKontextSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="— Quelle wählen —"
|
||||
placeholder={t('clickUpNodeConfig.quelleWaehlen')}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -552,7 +556,7 @@ function ClickUpCustomFieldRow({
|
|||
else onChange(createValue(v));
|
||||
}}
|
||||
>
|
||||
<option value="">— Option wählen —</option>
|
||||
<option value="">{t('clickUpNodeConfig.optionWaehlen')}</option>
|
||||
{opts.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
|
|
@ -574,7 +578,7 @@ function ClickUpCustomFieldRow({
|
|||
<StatischKontextSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="— Quelle wählen —"
|
||||
placeholder={t('clickUpNodeConfig.quelleWaehlen')}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -610,7 +614,7 @@ function ClickUpCustomFieldRow({
|
|||
<StatischKontextSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="— Quelle wählen —"
|
||||
placeholder={t('clickUpNodeConfig.quelleWaehlen')}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -670,6 +674,7 @@ function ClickUpTaskDueDateRow({
|
|||
value: unknown;
|
||||
onChange: (v: unknown) => void;
|
||||
}) {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
const hasSources =
|
||||
dataFlow &&
|
||||
|
|
@ -685,13 +690,13 @@ function ClickUpTaskDueDateRow({
|
|||
const day = ms > 0 ? new Date(ms).toISOString().slice(0, 10) : '';
|
||||
return (
|
||||
<div className={styles.dynamicValueField}>
|
||||
<label>Fälligkeit</label>
|
||||
<label>{t('clickUpNodeConfig.faelligkeit')}</label>
|
||||
{hasSources ? (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<StatischKontextSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="— Quelle wählen —"
|
||||
placeholder={t('clickUpNodeConfig.quelleWaehlen')}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -700,8 +705,8 @@ function ClickUpTaskDueDateRow({
|
|||
type="date"
|
||||
value={day}
|
||||
onChange={(e) => {
|
||||
const t = e.target.value ? new Date(e.target.value).getTime() : '';
|
||||
onChange(createValue(t === '' ? '' : t));
|
||||
const msFromDate = e.target.value ? new Date(e.target.value).getTime() : '';
|
||||
onChange(createValue(msFromDate === '' ? '' : msFromDate));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -716,6 +721,7 @@ function ClickUpTimeEstimateHoursRow({
|
|||
value: unknown;
|
||||
onChange: (v: unknown) => void;
|
||||
}) {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
const hasSources =
|
||||
dataFlow &&
|
||||
|
|
@ -731,13 +737,13 @@ function ClickUpTimeEstimateHoursRow({
|
|||
const hours = h > 0 && Number.isFinite(h) ? h : '';
|
||||
return (
|
||||
<div className={styles.dynamicValueField}>
|
||||
<label>Zeitschätzung (Stunden)</label>
|
||||
<label>{t('clickUpNodeConfig.zeitschaetzungStunden')}</label>
|
||||
{hasSources ? (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<StatischKontextSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="— Quelle wählen —"
|
||||
placeholder={t('clickUpNodeConfig.quelleWaehlen')}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -778,17 +784,19 @@ function newClickUpTaskUpdateEntryRow(): ClickUpTaskUpdateEntryRow {
|
|||
};
|
||||
}
|
||||
|
||||
const UPDATE_TASK_FIELD_OPTIONS: Array<{ value: string; label: string }> = [
|
||||
{ value: 'name', label: 'Titel (name)' },
|
||||
{ value: 'description', label: 'Beschreibung' },
|
||||
{ value: 'status', label: 'Status' },
|
||||
{ value: 'priority', label: 'Priorität (1–4)' },
|
||||
{ value: 'due_date', label: 'Fälligkeit (Datum oder ms)' },
|
||||
{ value: 'time_estimate_h', label: 'Zeitschätzung (Stunden)' },
|
||||
{ value: 'time_estimate_ms', label: 'Zeitschätzung (ms)' },
|
||||
{ value: 'assignees', label: 'Zugewiesene' },
|
||||
{ value: 'custom_field', label: 'Benutzerdefiniertes Feld' },
|
||||
function _getUpdateTaskFieldOptions(t: (key: string) => string): Array<{ value: string; label: string }> {
|
||||
return [
|
||||
{ value: 'name', label: t('clickUpNodeConfig.titleName') },
|
||||
{ value: 'description', label: t('clickUpNodeConfig.description') },
|
||||
{ value: 'status', label: t('clickUpNodeConfig.status') },
|
||||
{ value: 'priority', label: t('clickUpNodeConfig.priority') },
|
||||
{ value: 'due_date', label: t('clickUpNodeConfig.dueDate') },
|
||||
{ value: 'time_estimate_h', label: t('clickUpNodeConfig.timeEstimateHours') },
|
||||
{ value: 'time_estimate_ms', label: t('clickUpNodeConfig.timeEstimateMs') },
|
||||
{ value: 'assignees', label: t('clickUpNodeConfig.assignees') },
|
||||
{ value: 'custom_field', label: t('clickUpNodeConfig.customField') },
|
||||
];
|
||||
}
|
||||
|
||||
function normalizeTaskUpdateEntries(raw: unknown): ClickUpTaskUpdateEntryRow[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
|
|
@ -828,6 +836,7 @@ function ClickUpTaskFromListDropdown({
|
|||
taskId: unknown;
|
||||
onSetTaskId: (v: unknown) => void;
|
||||
}) {
|
||||
const { t } = useLanguage();
|
||||
const [tasks, setTasks] = useState<Array<{ id: string; name: string }>>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
|
|
@ -864,12 +873,12 @@ function ClickUpTaskFromListDropdown({
|
|||
|
||||
return (
|
||||
<div className={styles.dynamicValueField} style={{ marginTop: 10 }}>
|
||||
<label>Vorhandene Aufgabe in der gewählten Liste</label>
|
||||
<label>{t('clickUpNodeConfig.vorhandeneAufgabeInDerGewaehlten')}</label>
|
||||
{err ? (
|
||||
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary, #c00)' }}>{err}</p>
|
||||
) : null}
|
||||
{loading ? (
|
||||
<span style={{ fontSize: '0.85rem', color: '#666' }}>Aufgaben werden geladen…</span>
|
||||
<span style={{ fontSize: '0.85rem', color: '#666' }}>{t('clickUpNodeConfig.aufgabenWerdenGeladen')}</span>
|
||||
) : (
|
||||
<select
|
||||
value={sel}
|
||||
|
|
@ -879,10 +888,10 @@ function ClickUpTaskFromListDropdown({
|
|||
else onSetTaskId(createValue(id));
|
||||
}}
|
||||
>
|
||||
<option value="">— Aufgabe aus ClickUp wählen —</option>
|
||||
{tasks.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.name || t.id}
|
||||
<option value="">{t('clickUpNodeConfig.aufgabeAusClickupWaehlen')}</option>
|
||||
{tasks.map((task) => (
|
||||
<option key={task.id} value={task.id}>
|
||||
{task.name || task.id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -923,12 +932,15 @@ function ClickUpUpdateTaskEntriesEditor({
|
|||
parentListStatuses: Array<{ status: string; orderindex: number }>;
|
||||
parentListStatusesLoading: boolean;
|
||||
}) {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
const [resolvedStatuses, setResolvedStatuses] = useState<Array<{ status: string; orderindex: number }>>(
|
||||
[]
|
||||
);
|
||||
const [resolvedStatusesLoading, setResolvedStatusesLoading] = useState(false);
|
||||
|
||||
const updateTaskFieldOptions = useMemo(() => _getUpdateTaskFieldOptions(t), [t]);
|
||||
|
||||
const previewFingerprint = useMemo(
|
||||
() => JSON.stringify(dataFlow?.nodeOutputsPreview ?? {}),
|
||||
[dataFlow?.nodeOutputsPreview]
|
||||
|
|
@ -1044,7 +1056,7 @@ function ClickUpUpdateTaskEntriesEditor({
|
|||
const fk = row.fieldKey;
|
||||
const commonHybrid = (multiline?: boolean, placeholder?: string, inputType?: 'text' | 'number') => (
|
||||
<HybridStaticRefField
|
||||
label="Oder Referenz (ohne Formular-Payload)"
|
||||
label={t('clickUpNodeConfig.oderReferenzOhneFormularpayload')}
|
||||
value={row.value}
|
||||
onChange={(v) => updateRow(row.rowId, { value: v })}
|
||||
multiline={multiline}
|
||||
|
|
@ -1065,7 +1077,7 @@ function ClickUpUpdateTaskEntriesEditor({
|
|||
pathPickMode="exclude_forms"
|
||||
/>
|
||||
<HybridStaticRefField
|
||||
label="Neuer Wert"
|
||||
label={t('clickUpNodeConfig.neuerWert')}
|
||||
value={row.value}
|
||||
onChange={(v) => updateRow(row.rowId, { value: v })}
|
||||
multiline
|
||||
|
|
@ -1080,10 +1092,10 @@ function ClickUpUpdateTaskEntriesEditor({
|
|||
if (isRef(row.value)) {
|
||||
return (
|
||||
<HybridStaticRefField
|
||||
label="Status (Referenz)"
|
||||
label={t('clickUpNodeConfig.statusReferenz')}
|
||||
value={row.value}
|
||||
onChange={(v) => updateRow(row.rowId, { value: v })}
|
||||
placeholder="Kontext mit Status-String"
|
||||
placeholder={t('clickUpNodeConfig.kontextMitStatusstring')}
|
||||
pathPickMode="exclude_forms"
|
||||
/>
|
||||
);
|
||||
|
|
@ -1094,16 +1106,16 @@ function ClickUpUpdateTaskEntriesEditor({
|
|||
if (statusOptionsLoading) {
|
||||
return (
|
||||
<div className={styles.dynamicValueField}>
|
||||
<label>Status (wie in ClickUp)</label>
|
||||
<label>{t('clickUpNodeConfig.statusWieInClickup')}</label>
|
||||
<select disabled value="">
|
||||
<option value="">Status werden geladen…</option>
|
||||
<option value="">{t('clickUpNodeConfig.statusWerdenGeladen')}</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={styles.dynamicValueField}>
|
||||
<label>Status (wie in ClickUp)</label>
|
||||
<label>{t('clickUpNodeConfig.statusWieInClickup')}</label>
|
||||
<select
|
||||
value={staticVal}
|
||||
onChange={(e) => updateRow(row.rowId, { value: createValue(e.target.value) })}
|
||||
|
|
@ -1111,8 +1123,8 @@ function ClickUpUpdateTaskEntriesEditor({
|
|||
>
|
||||
<option value="">
|
||||
{statusOptions.length === 0
|
||||
? '— Keine Status geladen —'
|
||||
: '— Status wählen —'}
|
||||
? t('clickUpNodeConfig.keineStatusGeladen')
|
||||
: t('clickUpNodeConfig.statusWaehlen')}
|
||||
</option>
|
||||
{statusOptions.map((s) => (
|
||||
<option key={s.status} value={s.status}>
|
||||
|
|
@ -1144,16 +1156,16 @@ function ClickUpUpdateTaskEntriesEditor({
|
|||
const pv = isRef(row.value) ? '' : isValue(row.value) ? String(row.value.value ?? '') : String(row.value ?? '');
|
||||
return (
|
||||
<div className={styles.dynamicValueField}>
|
||||
<label>Priorität</label>
|
||||
<label>{t('clickUpNodeConfig.prioritaet')}</label>
|
||||
<select
|
||||
value={pv && ['1', '2', '3', '4'].includes(pv) ? pv : ''}
|
||||
onChange={(e) => updateRow(row.rowId, { value: createValue(e.target.value) })}
|
||||
>
|
||||
<option value="">— wählen —</option>
|
||||
<option value="1">1 — Dringend</option>
|
||||
<option value="2">2 — Hoch</option>
|
||||
<option value="3">3 — Normal</option>
|
||||
<option value="4">4 — Niedrig</option>
|
||||
<option value="">{t('clickUpNodeConfig.waehlen')}</option>
|
||||
<option value="1">{t('clickUpNodeConfig.1Dringend')}</option>
|
||||
<option value="2">{t('clickUpNodeConfig.2Hoch')}</option>
|
||||
<option value="3">{t('clickUpNodeConfig.3Normal')}</option>
|
||||
<option value="4">{t('clickUpNodeConfig.4Niedrig')}</option>
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1192,7 +1204,7 @@ function ClickUpUpdateTaskEntriesEditor({
|
|||
return (
|
||||
<>
|
||||
{teamMembersLoading ? (
|
||||
<span style={{ fontSize: '0.85rem', color: '#666' }}>Mitglieder werden geladen…</span>
|
||||
<span style={{ fontSize: '0.85rem', color: '#666' }}>{t('clickUpNodeConfig.mitgliederWerdenGeladen')}</span>
|
||||
) : teamMembers.length === 0 ? (
|
||||
<p style={{ fontSize: '0.8rem', color: '#666' }}>
|
||||
Keine Mitglieder — Workspace oben wählen oder JSON / Referenz unten.
|
||||
|
|
@ -1243,7 +1255,7 @@ function ClickUpUpdateTaskEntriesEditor({
|
|||
value={row.value}
|
||||
onChange={(v) => updateRow(row.rowId, { value: v })}
|
||||
multiline
|
||||
placeholder="Beschreibung"
|
||||
placeholder={t('clickUpNodeConfig.beschreibung')}
|
||||
pathPickMode="exclude_forms"
|
||||
/>
|
||||
);
|
||||
|
|
@ -1304,7 +1316,7 @@ function ClickUpUpdateTaskEntriesEditor({
|
|||
})
|
||||
}
|
||||
>
|
||||
{UPDATE_TASK_FIELD_OPTIONS.map((o) => (
|
||||
{updateTaskFieldOptions.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
|
|
@ -1336,6 +1348,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
mergeNodeParameters,
|
||||
nodeType = 'clickup.listTasks',
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
const lastFormSyncSigRef = useRef<string>('');
|
||||
const [connections, setConnections] = useState<UserConnection[]>([]);
|
||||
|
|
@ -1651,13 +1664,13 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
return (
|
||||
<>
|
||||
<div>
|
||||
<label>Connection (ClickUp)</label>
|
||||
<label>{t('clickUpNodeConfig.connectionClickup')}</label>
|
||||
<select
|
||||
value={connectionId}
|
||||
onChange={(e) => updateParam('connectionId', e.target.value)}
|
||||
disabled={connectionsLoading}
|
||||
>
|
||||
<option value="">{connectionsLoading ? 'Loading...' : 'Select connection'}</option>
|
||||
<option value="">{connectionsLoading ? t('clickUpNodeConfig.loading') : t('clickUpNodeConfig.selectConnection')}</option>
|
||||
{clickupConnections.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.externalUsername ?? c.id}
|
||||
|
|
@ -1680,7 +1693,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
disabled={searchTeamsLoading || !connectionId}
|
||||
>
|
||||
<option value="">
|
||||
{searchTeamsLoading ? 'Workspaces werden geladen…' : 'Workspace wählen…'}
|
||||
{searchTeamsLoading ? t('clickUpNodeConfig.workspacesWerdenGeladen') : t('clickUpNodeConfig.workspaceWaehlen')}
|
||||
</option>
|
||||
{searchTeams.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
|
|
@ -1690,7 +1703,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Liste (Tabelle)</label>
|
||||
<label>{t('clickUpNodeConfig.listeTabelle')}</label>
|
||||
<select
|
||||
value={listIdParam}
|
||||
onChange={(e) => updateParam('listId', e.target.value)}
|
||||
|
|
@ -1700,8 +1713,8 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
{searchListsLoading
|
||||
? 'Listen werden geladen…'
|
||||
: teamIdParam
|
||||
? 'Alle Listen im Workspace'
|
||||
: 'Zuerst Workspace wählen'}
|
||||
? t('clickUpNodeConfig.alleListenImWorkspace')
|
||||
: t('clickUpNodeConfig.zuerstWorkspaceWaehlen')}
|
||||
</option>
|
||||
{searchLists.map((L) => (
|
||||
<option key={L.id} value={L.id}>
|
||||
|
|
@ -1715,7 +1728,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
<input
|
||||
value={(params.query as string) ?? ''}
|
||||
onChange={(e) => updateParam('query', e.target.value)}
|
||||
placeholder="Stichwort für die Aufgabensuche"
|
||||
placeholder={t('clickUpNodeConfig.stichwortFuerDieAufgabensuche')}
|
||||
/>
|
||||
</div>
|
||||
<details style={{ marginTop: 8 }}>
|
||||
|
|
@ -1723,7 +1736,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
Erweitert
|
||||
</summary>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<label>Seite (Pagination)</label>
|
||||
<label>{t('clickUpNodeConfig.seitePagination')}</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
|
|
@ -1751,7 +1764,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
checked={Boolean(params.includeClosed)}
|
||||
onChange={(e) => updateParam('includeClosed', e.target.checked)}
|
||||
/>
|
||||
<span>Erledigte Aufgaben einbeziehen (Listen-Suche)</span>
|
||||
<span>{t('clickUpNodeConfig.erledigteAufgabenEinbeziehenListensuche')}</span>
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
@ -1790,7 +1803,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
disabled={searchTeamsLoading || !connectionId}
|
||||
>
|
||||
<option value="">
|
||||
{searchTeamsLoading ? 'Workspaces werden geladen…' : 'Workspace wählen…'}
|
||||
{searchTeamsLoading ? t('clickUpNodeConfig.workspacesWerdenGeladen') : t('clickUpNodeConfig.workspaceWaehlen')}
|
||||
</option>
|
||||
{searchTeams.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
|
|
@ -1816,7 +1829,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
}}
|
||||
disabled={!teamIdParam || searchListsLoading}
|
||||
>
|
||||
<option value="">{searchListsLoading ? 'Listen werden geladen…' : 'Liste wählen…'}</option>
|
||||
<option value="">{searchListsLoading ? t('clickUpNodeConfig.listenWerdenGeladen') : t('clickUpNodeConfig.listeWaehlen')}</option>
|
||||
{searchLists.map((L) => (
|
||||
<option key={L.id} value={L.id}>
|
||||
{L.name}
|
||||
|
|
@ -1851,7 +1864,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
checked={params.autoSyncFormWithList !== false}
|
||||
onChange={(e) => updateParam('autoSyncFormWithList', e.target.checked)}
|
||||
/>
|
||||
<span>Bei Listenwahl automatisch abgleichen</span>
|
||||
<span>{t('clickUpNodeConfig.beiListenwahlAutomatischAbgleichen')}</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -1887,15 +1900,15 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
</p>
|
||||
|
||||
<div className={styles.dynamicValueField}>
|
||||
<label>Status</label>
|
||||
<label>{t('clickUpNodeConfig.status')}</label>
|
||||
{listStatusesLoading ? (
|
||||
<span style={{ fontSize: '0.85rem', color: '#666' }}>Status wird geladen…</span>
|
||||
<span style={{ fontSize: '0.85rem', color: '#666' }}>{t('clickUpNodeConfig.statusWirdGeladen')}</span>
|
||||
) : (
|
||||
<select
|
||||
value={(params.taskStatus as string) ?? ''}
|
||||
onChange={(e) => updateParam('taskStatus', e.target.value)}
|
||||
>
|
||||
<option value="">— Standard (ClickUp) —</option>
|
||||
<option value="">{t('clickUpNodeConfig.standardClickup')}</option>
|
||||
{listStatuses.map((s) => (
|
||||
<option key={s.status} value={s.status}>
|
||||
{s.status}
|
||||
|
|
@ -1906,16 +1919,16 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
</div>
|
||||
|
||||
<div className={styles.dynamicValueField} style={{ marginTop: 8 }}>
|
||||
<label>Priorität</label>
|
||||
<label>{t('clickUpNodeConfig.prioritaet')}</label>
|
||||
<select
|
||||
value={(params.taskPriority as string) ?? ''}
|
||||
onChange={(e) => updateParam('taskPriority', e.target.value)}
|
||||
>
|
||||
<option value="">— keine —</option>
|
||||
<option value="1">1 — Dringend</option>
|
||||
<option value="2">2 — Hoch</option>
|
||||
<option value="3">3 — Normal</option>
|
||||
<option value="4">4 — Niedrig</option>
|
||||
<option value="">{t('clickUpNodeConfig.keine')}</option>
|
||||
<option value="1">{t('clickUpNodeConfig.1Dringend')}</option>
|
||||
<option value="2">{t('clickUpNodeConfig.2Hoch')}</option>
|
||||
<option value="3">{t('clickUpNodeConfig.3Normal')}</option>
|
||||
<option value="4">{t('clickUpNodeConfig.4Niedrig')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
@ -1929,9 +1942,9 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
<div className={styles.dynamicValueField} style={{ marginTop: 8 }}>
|
||||
<label>Zugewiesene</label>
|
||||
{teamMembersLoading ? (
|
||||
<span style={{ fontSize: '0.85rem', color: '#666' }}>Mitglieder werden geladen…</span>
|
||||
<span style={{ fontSize: '0.85rem', color: '#666' }}>{t('clickUpNodeConfig.mitgliederWerdenGeladen')}</span>
|
||||
) : teamMembers.length === 0 ? (
|
||||
<span style={{ fontSize: '0.85rem', color: '#666' }}>Keine Mitglieder geladen.</span>
|
||||
<span style={{ fontSize: '0.85rem', color: '#666' }}>{t('clickUpNodeConfig.keineMitgliederGeladen')}</span>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -1978,9 +1991,9 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
|
||||
{listIdParam ? (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<label style={{ fontWeight: 600 }}>Felder der Liste</label>
|
||||
<label style={{ fontWeight: 600 }}>{t('clickUpNodeConfig.felderDerListe')}</label>
|
||||
{listFieldsLoading ? (
|
||||
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>Felder werden geladen…</p>
|
||||
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>{t('clickUpNodeConfig.felderWerdenGeladen')}</p>
|
||||
) : listFields.length === 0 ? (
|
||||
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
|
||||
Keine benutzerdefinierten Felder oder keine Berechtigung.
|
||||
|
|
@ -2010,7 +2023,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
Erweitert (JSON)
|
||||
</summary>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<label>Zusätzliche Felder (JSON)</label>
|
||||
<label>{t('clickUpNodeConfig.zusaetzlicheFelderJson')}</label>
|
||||
<textarea
|
||||
value={(params.taskFields as string) ?? ''}
|
||||
onChange={(e) => updateParam('taskFields', e.target.value)}
|
||||
|
|
@ -2025,7 +2038,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
{nodeType === 'clickup.listTasks' && (
|
||||
<>
|
||||
<div>
|
||||
<label>List path</label>
|
||||
<label>{t('clickUpNodeConfig.listPath')}</label>
|
||||
<input
|
||||
value={path}
|
||||
onChange={(e) => updateParam('path', e.target.value)}
|
||||
|
|
@ -2066,11 +2079,11 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
label="Task-ID"
|
||||
value={params.taskId}
|
||||
onChange={(v) => updateParam('taskId', v)}
|
||||
placeholder="Referenz: voriger Knoten „Aufgabe erstellen“ → taskId"
|
||||
placeholder={t('clickUpNodeConfig.referenzVorigerKnotenAufgabeErstellen')}
|
||||
pathPickMode={nodeType === 'clickup.updateTask' ? 'clickup_task_id' : 'default'}
|
||||
/>
|
||||
<div>
|
||||
<label>Path (optional)</label>
|
||||
<label>{t('clickUpNodeConfig.pathOptional')}</label>
|
||||
<input
|
||||
value={path}
|
||||
onChange={(e) => updateParam('path', e.target.value)}
|
||||
|
|
@ -2119,7 +2132,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
disabled={searchTeamsLoading || !connectionId}
|
||||
>
|
||||
<option value="">
|
||||
{searchTeamsLoading ? 'Workspaces werden geladen…' : 'Workspace wählen…'}
|
||||
{searchTeamsLoading ? t('clickUpNodeConfig.workspacesWerdenGeladen') : t('clickUpNodeConfig.workspaceWaehlen')}
|
||||
</option>
|
||||
{searchTeams.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
|
|
@ -2143,7 +2156,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
}}
|
||||
disabled={!teamIdParam || searchListsLoading}
|
||||
>
|
||||
<option value="">{searchListsLoading ? 'Listen werden geladen…' : 'Liste wählen…'}</option>
|
||||
<option value="">{searchListsLoading ? t('clickUpNodeConfig.listenWerdenGeladen') : t('clickUpNodeConfig.listeWaehlen')}</option>
|
||||
{searchLists.map((L) => (
|
||||
<option key={L.id} value={L.id}>
|
||||
{L.name}
|
||||
|
|
@ -2174,7 +2187,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
Erweitert: JSON (optional, wird mit den Zeilen zusammengeführt)
|
||||
</summary>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<label>taskUpdate (JSON)</label>
|
||||
<label>{t('clickUpNodeConfig.taskupdateJson')}</label>
|
||||
<textarea
|
||||
value={(params.taskUpdate as string) ?? ''}
|
||||
onChange={(e) => updateParam('taskUpdate', e.target.value)}
|
||||
|
|
@ -2188,7 +2201,7 @@ export const ClickUpNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
|
||||
{nodeType === 'clickup.uploadAttachment' && (
|
||||
<div>
|
||||
<label>File name (optional)</label>
|
||||
<label>{t('clickUpNodeConfig.fileNameOptional')}</label>
|
||||
<input
|
||||
value={(params.fileName as string) ?? ''}
|
||||
onChange={(e) => updateParam('fileName', e.target.value)}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,18 @@
|
|||
import React from 'react';
|
||||
import type { NodeConfigRendererProps } from './types';
|
||||
|
||||
export const CommentNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => (
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export const CommentNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label>Platzhalter</label>
|
||||
<input
|
||||
value={(params.placeholder as string) ?? ''}
|
||||
onChange={(e) => updateParam('placeholder', e.target.value)}
|
||||
placeholder="Kommentar eingeben..."
|
||||
placeholder={t('commentNodeConfig.kommentarEingeben')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -27,3 +31,4 @@ export const CommentNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, u
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,18 +5,22 @@
|
|||
import React from 'react';
|
||||
import type { NodeConfigRendererProps } from './types';
|
||||
|
||||
export const ConfirmationNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => (
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export const ConfirmationNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<label>Frage</label>
|
||||
<input
|
||||
value={(params.question as string) ?? ''}
|
||||
onChange={(e) => updateParam('question', e.target.value)}
|
||||
placeholder="Möchten Sie bestätigen?"
|
||||
placeholder={t('confirmationNodeConfig.moechtenSieBestaetigen')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Bestätigen-Button</label>
|
||||
<label>{t('confirmationNodeConfig.bestaetigenbutton')}</label>
|
||||
<input
|
||||
value={(params.confirmLabel as string) ?? 'Confirm'}
|
||||
onChange={(e) => updateParam('confirmLabel', e.target.value)}
|
||||
|
|
@ -31,3 +35,4 @@ export const ConfirmationNodeConfig: React.FC<NodeConfigRendererProps> = ({ para
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,13 +6,15 @@ import React, { useEffect, useState } from 'react';
|
|||
import type { NodeConfigRendererProps } from './types';
|
||||
import { fetchConnections, fetchBrowse, type UserConnection, type BrowseEntry } from '../../../../api/workflowApi';
|
||||
|
||||
export const EmailNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
||||
params,
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export const EmailNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||
updateParam,
|
||||
instanceId,
|
||||
request,
|
||||
nodeType = 'email.checkEmail',
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [connections, setConnections] = useState<UserConnection[]>([]);
|
||||
const [folders, setFolders] = useState<BrowseEntry[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -57,7 +59,7 @@ export const EmailNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
onChange={(e) => updateParam('connectionId', e.target.value)}
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="">{loading ? 'Loading...' : 'Select connection'}</option>
|
||||
<option value="">{loading ? t('emailNodeConfig.loading') : t('emailNodeConfig.selectConnection')}</option>
|
||||
{connections.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.externalEmail ?? c.externalUsername ?? c.id}
|
||||
|
|
@ -74,7 +76,7 @@ export const EmailNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
disabled={foldersLoading || !connectionId}
|
||||
>
|
||||
<option value="">
|
||||
{foldersLoading ? 'Loading folders...' : !connectionId ? 'Select account first' : 'Select folder'}
|
||||
{foldersLoading ? 'Loading folders...' : !connectionId ? t('emailNodeConfig.selectAccountFirst') : t('emailNodeConfig.selectFolder')}
|
||||
</option>
|
||||
{isSearch && <option value="All">All</option>}
|
||||
{folders.length > 0
|
||||
|
|
@ -92,9 +94,9 @@ export const EmailNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
<>
|
||||
<option value="Inbox">Inbox</option>
|
||||
<option value="Drafts">Drafts</option>
|
||||
<option value="SentItems">Sent Items</option>
|
||||
<option value="DeletedItems">Deleted Items</option>
|
||||
<option value="JunkEmail">Junk Email</option>
|
||||
<option value="SentItems">{t('emailNodeConfig.sentItems')}</option>
|
||||
<option value="DeletedItems">{t('emailNodeConfig.deletedItems')}</option>
|
||||
<option value="JunkEmail">{t('emailNodeConfig.junkEmail')}</option>
|
||||
</>
|
||||
)}
|
||||
{folderValue &&
|
||||
|
|
@ -111,43 +113,43 @@ export const EmailNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
{isSearch && (
|
||||
<>
|
||||
<div>
|
||||
<label>Search query (optional)</label>
|
||||
<label>{t('emailNodeConfig.searchQueryOptional')}</label>
|
||||
<input
|
||||
value={(params.query as string) ?? ''}
|
||||
onChange={(e) => updateParam('query', e.target.value)}
|
||||
placeholder="General search term (subject, body, from)"
|
||||
placeholder={t('emailNodeConfig.generalSearchTermSubjectBody')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>From address (optional)</label>
|
||||
<label>{t('emailNodeConfig.fromAddressOptional')}</label>
|
||||
<input
|
||||
value={(params.fromAddress as string) ?? ''}
|
||||
onChange={(e) => updateParam('fromAddress', e.target.value)}
|
||||
placeholder="e.g. sender@example.com"
|
||||
placeholder={t('emailNodeConfig.egSenderexamplecom')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>To address (optional)</label>
|
||||
<label>{t('emailNodeConfig.toAddressOptional')}</label>
|
||||
<input
|
||||
value={(params.toAddress as string) ?? ''}
|
||||
onChange={(e) => updateParam('toAddress', e.target.value)}
|
||||
placeholder="e.g. recipient@example.com"
|
||||
placeholder={t('emailNodeConfig.egRecipientexamplecom')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Subject contains (optional)</label>
|
||||
<label>{t('emailNodeConfig.subjectContainsOptional')}</label>
|
||||
<input
|
||||
value={(params.subjectContains as string) ?? ''}
|
||||
onChange={(e) => updateParam('subjectContains', e.target.value)}
|
||||
placeholder="Word or phrase in subject"
|
||||
placeholder={t('emailNodeConfig.wordOrPhraseInSubject')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Body/content contains (optional)</label>
|
||||
<label>{t('emailNodeConfig.bodycontentContainsOptional')}</label>
|
||||
<input
|
||||
value={(params.bodyContains as string) ?? ''}
|
||||
onChange={(e) => updateParam('bodyContains', e.target.value)}
|
||||
placeholder="Word or phrase in email body"
|
||||
placeholder={t('emailNodeConfig.wordOrPhraseInEmail')}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
|
|
@ -157,7 +159,7 @@ export const EmailNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
checked={!!(params.hasAttachment as boolean)}
|
||||
onChange={(e) => updateParam('hasAttachment', e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="searchHasAttachment">Only emails with attachment</label>
|
||||
<label htmlFor="searchHasAttachment">{t('emailNodeConfig.onlyEmailsWithAttachment')}</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>Limit</label>
|
||||
|
|
@ -172,19 +174,19 @@ export const EmailNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
{nodeType === 'email.checkEmail' && (
|
||||
<>
|
||||
<div>
|
||||
<label>From address (optional)</label>
|
||||
<label>{t('emailNodeConfig.fromAddressOptional')}</label>
|
||||
<input
|
||||
value={(params.fromAddress as string) ?? ''}
|
||||
onChange={(e) => updateParam('fromAddress', e.target.value)}
|
||||
placeholder="e.g. sender@example.com"
|
||||
placeholder={t('emailNodeConfig.egSenderexamplecom')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Subject contains (optional)</label>
|
||||
<label>{t('emailNodeConfig.subjectContainsOptional')}</label>
|
||||
<input
|
||||
value={(params.subjectContains as string) ?? ''}
|
||||
onChange={(e) => updateParam('subjectContains', e.target.value)}
|
||||
placeholder="Word or phrase in subject"
|
||||
placeholder={t('emailNodeConfig.wordOrPhraseInSubject')}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
|
||||
|
|
@ -194,7 +196,7 @@ export const EmailNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
checked={!!(params.hasAttachment as boolean)}
|
||||
onChange={(e) => updateParam('hasAttachment', e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="hasAttachment">Only emails with attachment</label>
|
||||
<label htmlFor="hasAttachment">{t('emailNodeConfig.onlyEmailsWithAttachment')}</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>Limit</label>
|
||||
|
|
@ -213,7 +215,7 @@ export const EmailNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
<input
|
||||
value={(params.subject as string) ?? ''}
|
||||
onChange={(e) => updateParam('subject', e.target.value)}
|
||||
placeholder="Email subject (or leave empty if connected to AI node above)"
|
||||
placeholder={t('emailNodeConfig.emailSubjectOrLeaveEmpty')}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -221,16 +223,16 @@ export const EmailNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
<textarea
|
||||
value={(params.body as string) ?? ''}
|
||||
onChange={(e) => updateParam('body', e.target.value)}
|
||||
placeholder="Email body (or leave empty if connected to AI node above)"
|
||||
placeholder={t('emailNodeConfig.emailBodyOrLeaveEmpty')}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>To (optional)</label>
|
||||
<label>{t('emailNodeConfig.toOptional')}</label>
|
||||
<input
|
||||
value={(params.to as string) ?? ''}
|
||||
onChange={(e) => updateParam('to', e.target.value)}
|
||||
placeholder="Recipient(s) (or from AI when connected)"
|
||||
placeholder={t('emailNodeConfig.recipientsOrFromAiWhen')}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { RefSourceSelect } from '../shared/RefSourceSelect';
|
|||
import { isRef, type DataRef } from '../shared/dataRef';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
const OUTPUT_FORMATS = ['docx', 'pdf', 'txt', 'md', 'html', 'xlsx', 'csv', 'json'];
|
||||
const TEMPLATE_OPTIONS = ['default', 'corporate', 'minimal'];
|
||||
const LANGUAGES = ['de', 'en', 'fr', 'it', 'es'];
|
||||
|
|
@ -22,6 +24,7 @@ function normalizeContentSources(v: unknown): (DataRef | null)[] {
|
|||
}
|
||||
|
||||
export const FileCreateNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||
const { t } = useLanguage();
|
||||
const contentSources = normalizeContentSources(params.contentSources ?? params.contentSource ?? []);
|
||||
|
||||
const setContentSources = (next: (DataRef | null)[]) => {
|
||||
|
|
@ -41,20 +44,20 @@ export const FileCreateNodeConfig: React.FC<NodeConfigRendererProps> = ({ params
|
|||
return (
|
||||
<>
|
||||
<div className={styles.fileCreateContentSources}>
|
||||
<label>Inhalte (welche Kontexte nacheinander in die Datei?)</label>
|
||||
<label>{t('fileCreateNodeConfig.inhalteWelcheKontexteNacheinanderIn')}</label>
|
||||
{contentSources.map((ref, i) => (
|
||||
<div key={i} className={styles.contentSourceRow}>
|
||||
<RefSourceSelect
|
||||
value={ref}
|
||||
onChange={(r) => setItem(i, r)}
|
||||
placeholder="Quelle wählen…"
|
||||
placeholder={t('fileCreateNodeConfig.quelleWaehlen')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.contentSourceRemoveBtn}
|
||||
onClick={() => removeItem(i)}
|
||||
title="Entfernen"
|
||||
aria-label="Inhalt entfernen"
|
||||
title={t('fileCreateNodeConfig.entfernen')}
|
||||
aria-label={t('fileCreateNodeConfig.inhaltEntfernen')}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
|
@ -91,7 +94,7 @@ export const FileCreateNodeConfig: React.FC<NodeConfigRendererProps> = ({ params
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Vorlage / Stil</label>
|
||||
<label>{t('fileCreateNodeConfig.vorlageStil')}</label>
|
||||
<select
|
||||
value={(params.templateName as string) ?? 'default'}
|
||||
onChange={(e) => updateParam('templateName', e.target.value)}
|
||||
|
|
@ -104,7 +107,7 @@ export const FileCreateNodeConfig: React.FC<NodeConfigRendererProps> = ({ params
|
|||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Sprache</label>
|
||||
<label>{t('fileCreateNodeConfig.sprache')}</label>
|
||||
<select
|
||||
value={(params.language as string) ?? 'de'}
|
||||
onChange={(e) => updateParam('language', e.target.value)}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import type { NodeConfigRendererProps } from './types';
|
|||
import { fetchConnections, fetchBrowse, type UserConnection, type BrowseEntry } from '../../../../api/workflowApi';
|
||||
import { SharepointBrowseTree } from '../../../FolderTree/SharepointBrowseTree';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
const browseDetailsStyle: React.CSSProperties = {
|
||||
marginTop: 12,
|
||||
border: '1px solid var(--border-color, #e0e0e0)',
|
||||
|
|
@ -58,13 +60,13 @@ function isFolderPickerNode(nodeType: string): boolean {
|
|||
return nodeType === 'sharepoint.uploadFile' || nodeType === 'sharepoint.listFiles';
|
||||
}
|
||||
|
||||
export const SharePointNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
||||
params,
|
||||
export const SharePointNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||
updateParam,
|
||||
instanceId,
|
||||
request,
|
||||
nodeType = 'sharepoint.findFile',
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [connections, setConnections] = useState<UserConnection[]>([]);
|
||||
const [browseExpanded, setBrowseExpanded] = useState(false);
|
||||
const [findFileBrowseExpanded, setFindFileBrowseExpanded] = useState(false);
|
||||
|
|
@ -153,7 +155,7 @@ export const SharePointNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
onChange={(e) => updateParam('connectionId', e.target.value)}
|
||||
disabled={connectionsLoading}
|
||||
>
|
||||
<option value="">{connectionsLoading ? 'Loading...' : 'Select connection'}</option>
|
||||
<option value="">{connectionsLoading ? t('sharePointNodeConfig.loading') : t('sharePointNodeConfig.selectConnection')}</option>
|
||||
{connections.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.externalUsername ?? c.id}
|
||||
|
|
@ -164,7 +166,7 @@ export const SharePointNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
|
||||
{needsSearch && (
|
||||
<div>
|
||||
<label>Search query / path</label>
|
||||
<label>{t('sharePointNodeConfig.searchQueryPath')}</label>
|
||||
<input
|
||||
value={(params.searchQuery as string) ?? ''}
|
||||
onChange={(e) => updateParam('searchQuery', e.target.value)}
|
||||
|
|
@ -175,7 +177,7 @@ export const SharePointNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
|
||||
{showPathFieldsForList && (
|
||||
<div>
|
||||
<label>Folder path</label>
|
||||
<label>{t('sharePointNodeConfig.folderPath')}</label>
|
||||
<input
|
||||
value={path}
|
||||
onChange={(e) => updateParam('path', e.target.value)}
|
||||
|
|
@ -190,8 +192,8 @@ export const SharePointNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
{nodeType === 'sharepoint.uploadFile'
|
||||
? 'Target folder path'
|
||||
: nodeType === 'sharepoint.downloadFile'
|
||||
? 'File path'
|
||||
: 'Path'}
|
||||
? t('sharePointNodeConfig.filePath')
|
||||
: t('sharePointNodeConfig.path')}
|
||||
</label>
|
||||
<input
|
||||
value={(params.path as string) ?? (params.filePath as string) ?? ''}
|
||||
|
|
@ -209,11 +211,11 @@ export const SharePointNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
|
||||
{needsSiteId && (
|
||||
<div>
|
||||
<label>Site ID</label>
|
||||
<label>{t('sharePointNodeConfig.siteId')}</label>
|
||||
<input
|
||||
value={(params.siteId as string) ?? ''}
|
||||
onChange={(e) => updateParam('siteId', e.target.value)}
|
||||
placeholder="SharePoint site ID"
|
||||
placeholder={t('sharePointNodeConfig.sharepointSiteId')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -221,7 +223,7 @@ export const SharePointNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
{nodeType === 'sharepoint.copyFile' && (
|
||||
<>
|
||||
<div>
|
||||
<label>Source file</label>
|
||||
<label>{t('sharePointNodeConfig.sourceFile')}</label>
|
||||
<input
|
||||
value={(params.sourcePath as string) ?? ''}
|
||||
onChange={(e) => updateParam('sourcePath', e.target.value)}
|
||||
|
|
@ -229,7 +231,7 @@ export const SharePointNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Destination folder</label>
|
||||
<label>{t('sharePointNodeConfig.destinationFolder')}</label>
|
||||
<input
|
||||
value={(params.destPath as string) ?? ''}
|
||||
onChange={(e) => updateParam('destPath', e.target.value)}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,15 @@ import React, { useEffect, useState } from 'react';
|
|||
import type { NodeConfigRendererProps } from './types';
|
||||
import { fetchConnections, type UserConnection } from '../../../../api/workflowApi';
|
||||
|
||||
export const TrusteeNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
||||
params,
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export const TrusteeNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||
updateParam,
|
||||
instanceId,
|
||||
request,
|
||||
nodeType = 'trustee.extractFromFiles',
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [connections, setConnections] = useState<UserConnection[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
|
|
@ -32,24 +34,24 @@ export const TrusteeNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
return (
|
||||
<>
|
||||
<div>
|
||||
<label>Trustee Instance ID</label>
|
||||
<label>{t('trusteeNodeConfig.trusteeInstanceId')}</label>
|
||||
<input
|
||||
value={(params.featureInstanceId as string) ?? ''}
|
||||
onChange={(e) => updateParam('featureInstanceId', e.target.value)}
|
||||
placeholder="Trustee Feature-Instanz-ID"
|
||||
placeholder={t('trusteeNodeConfig.trusteeFeatureinstanzid')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isExtract && (
|
||||
<>
|
||||
<div>
|
||||
<label>SharePoint Connection (optional)</label>
|
||||
<label>{t('trusteeNodeConfig.sharepointConnectionOptional')}</label>
|
||||
<select
|
||||
value={(params.connectionId as string) ?? ''}
|
||||
onChange={(e) => updateParam('connectionId', e.target.value)}
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="">{loading ? 'Laden...' : 'Keine (Dateien aus vorherigem Schritt)'}</option>
|
||||
<option value="">{loading ? t('trusteeNodeConfig.laden') : t('trusteeNodeConfig.keineDateienAusVorherigemSchritt')}</option>
|
||||
{connections.map((c) => (
|
||||
<option key={c.id} value={c.id}>
|
||||
{c.externalUsername ?? c.id}
|
||||
|
|
@ -58,7 +60,7 @@ export const TrusteeNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>SharePoint Ordnerpfad (optional)</label>
|
||||
<label>{t('trusteeNodeConfig.sharepointOrdnerpfadOptional')}</label>
|
||||
<input
|
||||
value={(params.sharepointFolder as string) ?? ''}
|
||||
onChange={(e) => updateParam('sharepointFolder', e.target.value)}
|
||||
|
|
@ -66,11 +68,11 @@ export const TrusteeNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>AI Prompt (optional)</label>
|
||||
<label>{t('trusteeNodeConfig.aiPromptOptional')}</label>
|
||||
<textarea
|
||||
value={(params.prompt as string) ?? ''}
|
||||
onChange={(e) => updateParam('prompt', e.target.value)}
|
||||
placeholder="Zusätzliche Anweisungen für die AI-Extraktion"
|
||||
placeholder={t('trusteeNodeConfig.zusaetzlicheAnweisungenFuerDieAiextraktion')}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -79,11 +81,11 @@ export const TrusteeNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
|
||||
{!isExtract && (
|
||||
<div>
|
||||
<label>Document List (Referenz)</label>
|
||||
<label>{t('trusteeNodeConfig.documentListReferenz')}</label>
|
||||
<input
|
||||
value={(params.documentList as string) ?? ''}
|
||||
onChange={(e) => updateParam('documentList', e.target.value)}
|
||||
placeholder="Referenz auf vorherigen Schritt (automatisch verknüpft)"
|
||||
placeholder={t('trusteeNodeConfig.referenzAufVorherigenSchrittAutomatisch')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import type { NodeConfigRendererProps } from './types';
|
|||
import { getAcceptValues, parseAllowedTypes } from '../runtime/fileTypeMimeMapping';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
function buildAcceptString(allowedTypes: string[]): string {
|
||||
if (allowedTypes.length === 0) return '';
|
||||
return allowedTypes.join(',');
|
||||
|
|
@ -22,6 +24,7 @@ export function getAcceptStringFromConfig(config: Record<string, unknown>): stri
|
|||
const FILE_TYPE_CHIP_OPTIONS = getAcceptValues();
|
||||
|
||||
export const UploadNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||
const { t } = useLanguage();
|
||||
const allowedTypes = parseAllowedTypes(params);
|
||||
const maxSize = (params.maxSize as number) ?? 10;
|
||||
const multiple = (params.multiple as boolean) ?? false;
|
||||
|
|
@ -37,7 +40,7 @@ export const UploadNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
return (
|
||||
<div className={styles.uploadNodeConfig}>
|
||||
<div className={styles.configBlock}>
|
||||
<label>Erlaubte Dateitypen</label>
|
||||
<label>{t('uploadNodeConfig.erlaubteDateitypen')}</label>
|
||||
<p className={styles.configHint}>
|
||||
Mehrfachauswahl möglich. Keine Auswahl = alle Typen erlaubt.
|
||||
</p>
|
||||
|
|
@ -55,7 +58,7 @@ export const UploadNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
</div>
|
||||
</div>
|
||||
<div className={styles.configBlock}>
|
||||
<label>Max. Größe (MB)</label>
|
||||
<label>{t('uploadNodeConfig.maxGroesseMb')}</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0.1}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ import type { FormField, NodeConfigRendererProps } from '../configs/types';
|
|||
import { fetchConnections, type UserConnection } from '../../../../api/workflowApi';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
||||
params,
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
||||
updateParam,
|
||||
instanceId,
|
||||
request,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const fields = (params.fields as FormField[]) ?? [];
|
||||
const [connections, setConnections] = useState<UserConnection[]>([]);
|
||||
const [connectionsLoading, setConnectionsLoading] = useState(false);
|
||||
|
|
@ -74,7 +76,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
<div className={styles.formFieldRowHeader}>
|
||||
<span
|
||||
className={styles.formFieldDragHandle}
|
||||
title="Zum Verschieben ziehen"
|
||||
title={t('formNodeConfig.zumVerschiebenZiehen')}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData('text/plain', String(i));
|
||||
|
|
@ -131,8 +133,8 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
<option value="number">Number</option>
|
||||
<option value="date">Date</option>
|
||||
<option value="boolean">Checkbox</option>
|
||||
<option value="clickup_tasks">ClickUp-Aufgabe (Referenz)</option>
|
||||
<option value="clickup_status">ClickUp-Status (Liste)</option>
|
||||
<option value="clickup_tasks">{t('formNodeConfig.clickupaufgabeReferenz')}</option>
|
||||
<option value="clickup_status">{t('formNodeConfig.clickupstatusListe')}</option>
|
||||
</select>
|
||||
<label className={styles.formFieldRequiredLabel}>
|
||||
<input
|
||||
|
|
@ -149,7 +151,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => removeField(i)}
|
||||
title="Feld entfernen"
|
||||
title={t('formNodeConfig.feldEntfernen')}
|
||||
className={styles.formFieldRemoveButton}
|
||||
>
|
||||
<FaTimes />
|
||||
|
|
@ -196,7 +198,7 @@ export const FormNodeConfig: React.FC<NodeConfigRendererProps> = ({
|
|||
Listen-ID (verknüpfte Liste / Ziel-Liste)
|
||||
</label>
|
||||
<input
|
||||
placeholder="z. B. aus ClickUp-URL …/list/123456789"
|
||||
placeholder={t('formNodeConfig.zBAusClickupurlList123456789')}
|
||||
value={f.clickupListId ?? ''}
|
||||
onChange={(e) => {
|
||||
const next = [...fields];
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ export type FieldRendererComponent = ComponentType<FieldRendererProps>;
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
const TextInput: React.FC<FieldRendererProps> = ({ param, value, onChange }) => (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||
|
|
@ -149,10 +151,11 @@ const JsonEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) =>
|
|||
const HiddenInput: React.FC<FieldRendererProps> = () => null;
|
||||
|
||||
const ConnectionPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, instanceId, request }) => {
|
||||
const { t } = useLanguage();
|
||||
const [connections, setConnections] = React.useState<Array<{ id: string; label: string }>>([]);
|
||||
React.useEffect(() => {
|
||||
if (!instanceId || !request) return;
|
||||
request(`/api/graphicalEditor/${instanceId}/options/user.connection`)
|
||||
request({ url: `/api/graphicalEditor/${instanceId}/options/user.connection`, method: 'get' })
|
||||
.then((res: unknown) => {
|
||||
const data = res as { options?: Array<{ value: string; label: string }> };
|
||||
setConnections((data?.options || []).map((o) => ({ id: o.value, label: o.label })));
|
||||
|
|
@ -167,7 +170,7 @@ const ConnectionPicker: React.FC<FieldRendererProps> = ({ param, value, onChange
|
|||
onChange={(e) => onChange(e.target.value)}
|
||||
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc' }}
|
||||
>
|
||||
<option value="">— Select connection —</option>
|
||||
<option value="">{t('index.selectConnection')}</option>
|
||||
{connections.map((c) => (
|
||||
<option key={c.id} value={c.id}>{c.label}</option>
|
||||
))}
|
||||
|
|
@ -196,6 +199,7 @@ const FolderPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, al
|
|||
};
|
||||
|
||||
const CaseListEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||
const { t } = useLanguage();
|
||||
const cases = Array.isArray(value) ? value : [];
|
||||
const addCase = () => onChange([...cases, { operator: 'eq', value: '' }]);
|
||||
const removeCase = (idx: number) => onChange(cases.filter((_: unknown, i: number) => i !== idx));
|
||||
|
|
@ -211,21 +215,22 @@ const CaseListEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }
|
|||
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
||||
<select value={String(c.operator || 'eq')} onChange={(e) => updateCase(i, 'operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
||||
<option value="eq">equals</option>
|
||||
<option value="neq">not equals</option>
|
||||
<option value="neq">{t('index.notEquals')}</option>
|
||||
<option value="contains">contains</option>
|
||||
<option value="gt">greater than</option>
|
||||
<option value="lt">less than</option>
|
||||
<option value="gt">{t('index.greaterThan')}</option>
|
||||
<option value="lt">{t('index.lessThan')}</option>
|
||||
</select>
|
||||
<input type="text" value={String(c.value ?? '')} onChange={(e) => updateCase(i, 'value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<button onClick={() => removeCase(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={addCase} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>+ Add case</button>
|
||||
<button onClick={addCase} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('index.addCase')}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||
const { t } = useLanguage();
|
||||
const fields = Array.isArray(value) ? value : [];
|
||||
const addField = () => onChange([...fields, { name: '', type: 'text', label: '', required: false }]);
|
||||
const removeField = (idx: number) => onChange(fields.filter((_: unknown, i: number) => i !== idx));
|
||||
|
|
@ -255,12 +260,13 @@ const FieldBuilderEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
|
|||
<button onClick={() => removeField(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={addField} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>+ Add field</button>
|
||||
<button onClick={addField} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('index.addField')}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const KeyValueRowsEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||
const { t } = useLanguage();
|
||||
const rows = Array.isArray(value) ? value : [];
|
||||
const addRow = () => onChange([...rows, { key: '', value: '' }]);
|
||||
const removeRow = (idx: number) => onChange(rows.filter((_: unknown, i: number) => i !== idx));
|
||||
|
|
@ -279,26 +285,30 @@ const KeyValueRowsEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
|
|||
<button onClick={() => removeRow(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={addRow} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>+ Add row</button>
|
||||
<button onClick={addRow} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('index.addRow')}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CronBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange }) => (
|
||||
const CronBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={typeof value === 'string' ? value : ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="*/5 * * * *"
|
||||
placeholder={t('index.5')}
|
||||
style={{ width: '100%', padding: '4px 8px', borderRadius: 4, border: '1px solid #ccc', fontFamily: 'monospace' }}
|
||||
/>
|
||||
<p style={{ fontSize: 10, color: '#888', margin: '2px 0 0' }}>Cron: min hour day month weekday</p>
|
||||
<p style={{ fontSize: 10, color: '#888', margin: '2px 0 0' }}>{t('index.cronMinHourDayMonth')}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ConditionBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||
const { t } = useLanguage();
|
||||
const cond = (typeof value === 'object' && value !== null) ? value as Record<string, unknown> : {};
|
||||
const update = (field: string, val: unknown) => onChange({ ...cond, type: 'condition', [field]: val });
|
||||
return (
|
||||
|
|
@ -307,14 +317,14 @@ const ConditionBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange
|
|||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<select value={String(cond.operator ?? 'eq')} onChange={(e) => update('operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
||||
<option value="eq">equals</option>
|
||||
<option value="neq">not equals</option>
|
||||
<option value="gt">greater than</option>
|
||||
<option value="lt">less than</option>
|
||||
<option value="neq">{t('index.notEquals')}</option>
|
||||
<option value="gt">{t('index.greaterThan')}</option>
|
||||
<option value="lt">{t('index.lessThan')}</option>
|
||||
<option value="contains">contains</option>
|
||||
<option value="empty">is empty</option>
|
||||
<option value="not_empty">is not empty</option>
|
||||
<option value="is_true">is true</option>
|
||||
<option value="is_false">is false</option>
|
||||
<option value="empty">{t('index.isEmpty')}</option>
|
||||
<option value="not_empty">{t('index.isNotEmpty')}</option>
|
||||
<option value="is_true">{t('index.isTrue')}</option>
|
||||
<option value="is_false">{t('index.isFalse')}</option>
|
||||
</select>
|
||||
<input type="text" placeholder="Value" value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 2, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
</div>
|
||||
|
|
@ -323,6 +333,7 @@ const ConditionBuilder: React.FC<FieldRendererProps> = ({ param, value, onChange
|
|||
};
|
||||
|
||||
const MappingTableEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||
const { t } = useLanguage();
|
||||
const mappings = Array.isArray(value) ? value : [];
|
||||
const addMapping = () => onChange([...mappings, { sourceField: '', outputField: '' }]);
|
||||
const removeMapping = (idx: number) => onChange(mappings.filter((_: unknown, i: number) => i !== idx));
|
||||
|
|
@ -336,18 +347,19 @@ const MappingTableEditor: React.FC<FieldRendererProps> = ({ param, value, onChan
|
|||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||
{mappings.map((m: Record<string, unknown>, i: number) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 4, marginBottom: 4 }}>
|
||||
<input type="text" placeholder="Source field" value={String(m.sourceField ?? '')} onChange={(e) => updateMapping(i, 'sourceField', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<input type="text" placeholder={t('index.sourceField')} value={String(m.sourceField ?? '')} onChange={(e) => updateMapping(i, 'sourceField', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<span style={{ alignSelf: 'center' }}>→</span>
|
||||
<input type="text" placeholder="Output field" value={String(m.outputField ?? '')} onChange={(e) => updateMapping(i, 'outputField', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<input type="text" placeholder={t('index.outputField')} value={String(m.outputField ?? '')} onChange={(e) => updateMapping(i, 'outputField', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<button onClick={() => removeMapping(i)} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer' }}>×</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={addMapping} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>+ Add mapping</button>
|
||||
<button onClick={addMapping} style={{ padding: '2px 8px', borderRadius: 4, border: '1px solid #ccc', cursor: 'pointer', fontSize: 12 }}>{t('index.addMapping')}</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FilterExpressionEditor: React.FC<FieldRendererProps> = ({ param, value, onChange }) => {
|
||||
const { t } = useLanguage();
|
||||
const cond = (typeof value === 'object' && value !== null) ? value as Record<string, unknown> : {};
|
||||
const update = (field: string, val: unknown) => onChange({ ...cond, [field]: val });
|
||||
return (
|
||||
|
|
@ -357,13 +369,13 @@ const FilterExpressionEditor: React.FC<FieldRendererProps> = ({ param, value, on
|
|||
<input type="text" placeholder="Field" value={String(cond.field ?? '')} onChange={(e) => update('field', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
<select value={String(cond.operator ?? 'eq')} onChange={(e) => update('operator', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }}>
|
||||
<option value="eq">equals</option>
|
||||
<option value="neq">not equals</option>
|
||||
<option value="neq">{t('index.notEquals')}</option>
|
||||
<option value="contains">contains</option>
|
||||
<option value="startsWith">starts with</option>
|
||||
<option value="isEmpty">is empty</option>
|
||||
<option value="isNotEmpty">is not empty</option>
|
||||
<option value="gt">greater than</option>
|
||||
<option value="lt">less than</option>
|
||||
<option value="startsWith">{t('index.startsWith')}</option>
|
||||
<option value="isEmpty">{t('index.isEmpty')}</option>
|
||||
<option value="isNotEmpty">{t('index.isNotEmpty')}</option>
|
||||
<option value="gt">{t('index.greaterThan')}</option>
|
||||
<option value="lt">{t('index.lessThan')}</option>
|
||||
</select>
|
||||
<input type="text" placeholder="Value" value={String(cond.value ?? '')} onChange={(e) => update('value', e.target.value)} style={{ flex: 1, padding: '2px 4px', borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import { getMimeTypeOptionsFromUploadParams } from '../runtime/fileTypeMimeMappi
|
|||
import { operatorsForType } from '../shared/conditionOperators';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export interface StructuredCondition {
|
||||
type: 'condition';
|
||||
ref: { type: 'ref'; nodeId: string; path: (string | number)[] } | null;
|
||||
|
|
@ -28,6 +30,7 @@ function parseCondition(v: unknown): StructuredCondition | null {
|
|||
}
|
||||
|
||||
export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
|
||||
const cond = parseCondition(params.condition);
|
||||
|
|
@ -97,7 +100,7 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
<div className={styles.ifElseConditionEditor}>
|
||||
<div className={styles.ifElseConditionRow}>
|
||||
<label>Datenquelle</label>
|
||||
<RefSourceSelect value={ref} onChange={handleRefChange} placeholder="Formular-Feld wählen…" />
|
||||
<RefSourceSelect value={ref} onChange={handleRefChange} placeholder={t('ifElseNodeConfig.formularfeldWaehlen')} />
|
||||
</div>
|
||||
<div className={styles.ifElseConditionRow}>
|
||||
<label>Vergleich</label>
|
||||
|
|
@ -117,7 +120,7 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
value={String(value ?? '')}
|
||||
onChange={(e) => handleValueChange(e.target.value)}
|
||||
>
|
||||
<option value="">— MIME-Type wählen —</option>
|
||||
<option value="">{t('ifElseNodeConfig.mimetypeWaehlen')}</option>
|
||||
{mimeTypeOptions.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label} ({o.value})
|
||||
|
|
@ -139,8 +142,8 @@ export const IfElseNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
: fieldType === 'date'
|
||||
? 'TT.MM.JJJJ'
|
||||
: isMimeTypeRef
|
||||
? 'z.B. application/pdf'
|
||||
: 'z.B. CH'
|
||||
? t('ifElseNodeConfig.zbApplicationpdf')
|
||||
: t('ifElseNodeConfig.zbCh')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext
|
|||
import type { NodeType, PortSchema } from '../../../../api/workflowApi';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
interface DataPickerProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
|
|
@ -95,8 +97,7 @@ function _resolveSchemaForNode(
|
|||
return _resolveSchemaForNode(incoming.source, nodes, nodeTypes, connections, catalog, visited);
|
||||
}
|
||||
|
||||
export const DataPicker: React.FC<DataPickerProps> = ({
|
||||
open,
|
||||
export const DataPicker: React.FC<DataPickerProps> = ({ open,
|
||||
onClose,
|
||||
onPick,
|
||||
availableSourceIds,
|
||||
|
|
@ -104,6 +105,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({
|
|||
nodeOutputsPreview,
|
||||
getNodeLabel,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
||||
const [showSystem, setShowSystem] = useState(false);
|
||||
const ctx = useAutomation2DataFlow();
|
||||
|
|
@ -113,7 +115,12 @@ export const DataPicker: React.FC<DataPickerProps> = ({
|
|||
const catalog = ctx?.portTypeCatalog ?? {};
|
||||
const systemVars = ctx?.systemVariables ?? {};
|
||||
const nodeTypes = ctx?.nodeTypes ?? [];
|
||||
const connections = ctx?.connections ?? [];
|
||||
const connectionsRaw = ctx?.connections ?? [];
|
||||
const connections = connectionsRaw.map((c) => ({
|
||||
source: c.sourceId,
|
||||
target: c.targetId,
|
||||
sourceOutput: c.sourceHandle,
|
||||
}));
|
||||
|
||||
const toggleExpand = (nodeId: string) => {
|
||||
setExpandedNodes((prev) => {
|
||||
|
|
@ -138,8 +145,8 @@ export const DataPicker: React.FC<DataPickerProps> = ({
|
|||
<div className={styles.dataPickerOverlay} onClick={onClose}>
|
||||
<div className={styles.dataPickerModal} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={styles.dataPickerHeader}>
|
||||
<h4 className={styles.dataPickerTitle}>Datenquelle wählen</h4>
|
||||
<button type="button" className={styles.dataPickerClose} onClick={onClose} aria-label="Schließen">
|
||||
<h4 className={styles.dataPickerTitle}>{t('dataPicker.datenquelleWaehlen')}</h4>
|
||||
<button type="button" className={styles.dataPickerClose} onClick={onClose} aria-label={t('dataPicker.schliessen')}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -180,7 +187,7 @@ export const DataPicker: React.FC<DataPickerProps> = ({
|
|||
return node?.type !== 'trigger.manual';
|
||||
});
|
||||
if (filteredIds.length === 0 && Object.keys(systemVars).length === 0) {
|
||||
return <p className={styles.dataPickerEmpty}>Keine vorherigen Nodes verfügbar.</p>;
|
||||
return <p className={styles.dataPickerEmpty}>{t('dataPicker.keineVorherigenNodesVerfuegbar')}</p>;
|
||||
}
|
||||
return filteredIds.map((nodeId) => {
|
||||
const node = nodes.find((n) => n.id === nodeId);
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@ import {
|
|||
createValue,
|
||||
formatRefLabel,
|
||||
type DataRef,
|
||||
type SystemVarRef,
|
||||
} from './dataRef';
|
||||
import { RefSourceSelect } from './RefSourceSelect';
|
||||
import { DataPicker } from './DataPicker';
|
||||
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export type FieldType = 'textarea' | 'input';
|
||||
|
||||
interface DynamicValueFieldProps {
|
||||
|
|
@ -28,14 +31,14 @@ interface DynamicValueFieldProps {
|
|||
variant?: 'picker' | 'dropdown';
|
||||
}
|
||||
|
||||
export const DynamicValueField: React.FC<DynamicValueFieldProps> = ({
|
||||
paramKey,
|
||||
export const DynamicValueField: React.FC<DynamicValueFieldProps> = ({ paramKey,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
placeholder,
|
||||
variant = 'picker',
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
|
||||
|
|
@ -47,7 +50,7 @@ export const DynamicValueField: React.FC<DynamicValueFieldProps> = ({
|
|||
});
|
||||
const canUseRef = dataFlow !== null && hasUsefulSources;
|
||||
|
||||
const handleSetRef = (newRef: DataRef | null) => {
|
||||
const handleSetRef = (newRef: DataRef | SystemVarRef | null) => {
|
||||
onChange(paramKey, newRef ?? createValue(''));
|
||||
};
|
||||
|
||||
|
|
@ -55,7 +58,7 @@ export const DynamicValueField: React.FC<DynamicValueFieldProps> = ({
|
|||
return (
|
||||
<div className={styles.dynamicValueField}>
|
||||
<label>{label}</label>
|
||||
<p className={styles.dynamicValueEmptyHint}>Keine vorherigen Nodes verfügbar.</p>
|
||||
<p className={styles.dynamicValueEmptyHint}>{t('dynamicValueField.keineVorherigenNodesVerfuegbar')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext
|
|||
import { isRef, isValue, createValue } from './dataRef';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
function parseHybrid(value: unknown): { staticStr: string } {
|
||||
if (isRef(value)) return { staticStr: '' };
|
||||
if (isValue(value)) {
|
||||
|
|
@ -37,8 +39,7 @@ export interface HybridStaticRefFieldProps {
|
|||
pathPickMode?: PathPickMode;
|
||||
}
|
||||
|
||||
export const HybridStaticRefField: React.FC<HybridStaticRefFieldProps> = ({
|
||||
label,
|
||||
export const HybridStaticRefField: React.FC<HybridStaticRefFieldProps> = ({ label,
|
||||
value,
|
||||
onChange,
|
||||
multiline,
|
||||
|
|
@ -46,6 +47,7 @@ export const HybridStaticRefField: React.FC<HybridStaticRefFieldProps> = ({
|
|||
placeholder,
|
||||
pathPickMode = 'default',
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
const hasSources =
|
||||
dataFlow &&
|
||||
|
|
@ -83,7 +85,7 @@ export const HybridStaticRefField: React.FC<HybridStaticRefFieldProps> = ({
|
|||
<StatischKontextSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder="— Quelle wählen —"
|
||||
placeholder={t('hybridStaticRefField.quelleWaehlen')}
|
||||
pathPickMode={pathPickMode}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { refToOptionValue, optionValueToRef } from './RefSourceSelect';
|
|||
import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
interface LoopOption {
|
||||
ref: DataRef;
|
||||
label: string;
|
||||
|
|
@ -153,11 +155,11 @@ interface LoopItemsSelectProps {
|
|||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({
|
||||
value,
|
||||
export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({ value,
|
||||
onChange,
|
||||
placeholder = 'Über was soll iteriert werden?',
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
if (!dataFlow) return null;
|
||||
|
||||
|
|
@ -182,7 +184,7 @@ export const LoopItemsSelect: React.FC<LoopItemsSelectProps> = ({
|
|||
|
||||
return (
|
||||
<div className={styles.ifElseConditionRow}>
|
||||
<label>Datenquelle für Iteration</label>
|
||||
<label>{t('loopItemsSelect.datenquelleFuerIteration')}</label>
|
||||
<select
|
||||
value={currentValue}
|
||||
onChange={(e) => {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import React, { useMemo } from 'react';
|
|||
import type { NodeConfigRendererProps } from '../configs/types';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
type FormField = {
|
||||
name: string;
|
||||
label: string;
|
||||
|
|
@ -15,17 +17,17 @@ type FormField = {
|
|||
|
||||
const FORM_FIELD_TYPES = ['text', 'number', 'email', 'date', 'boolean', 'clickup_status'] as const;
|
||||
|
||||
function parseFields(params: Record<string, unknown>): FormField[] {
|
||||
function _parseFields(params: Record<string, unknown>, t: (key: string) => string): FormField[] {
|
||||
const raw = params.formFields;
|
||||
if (!Array.isArray(raw)) return [{ name: 'field1', label: 'Feld 1', type: 'text' }];
|
||||
if (!Array.isArray(raw)) return [{ name: 'field1', label: t('formStartNodeConfig.field1'), type: 'text' }];
|
||||
return raw.map((f, i) => {
|
||||
if (f && typeof f === 'object' && !Array.isArray(f)) {
|
||||
const o = f as Record<string, unknown>;
|
||||
const t = String(o.type ?? 'text');
|
||||
const fieldType = String(o.type ?? 'text');
|
||||
const name = String(o.name ?? `field${i + 1}`);
|
||||
const label = String(o.label ?? `Feld ${i + 1}`);
|
||||
const type = (
|
||||
FORM_FIELD_TYPES.includes(t as (typeof FORM_FIELD_TYPES)[number]) ? t : 'text'
|
||||
FORM_FIELD_TYPES.includes(fieldType as (typeof FORM_FIELD_TYPES)[number]) ? fieldType : 'text'
|
||||
) as FormField['type'];
|
||||
if (type === 'clickup_status' && Array.isArray(o.statusOptions)) {
|
||||
return {
|
||||
|
|
@ -42,7 +44,8 @@ function parseFields(params: Record<string, unknown>): FormField[] {
|
|||
}
|
||||
|
||||
export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||
const fields = useMemo(() => parseFields(params), [params]);
|
||||
const { t } = useLanguage();
|
||||
const fields = useMemo(() => _parseFields(params, t), [params, t]);
|
||||
|
||||
const setFields = (next: FormField[]) => {
|
||||
updateParam('formFields', next);
|
||||
|
|
@ -59,7 +62,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
<div key={idx} className={styles.formFieldRow}>
|
||||
<input
|
||||
className={styles.startsInput}
|
||||
placeholder="Name (Payload-Key)"
|
||||
placeholder={t('formStartNodeConfig.namePayloadkey')}
|
||||
value={f.name}
|
||||
onChange={(e) => {
|
||||
const next = [...fields];
|
||||
|
|
@ -82,11 +85,11 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
value={f.type}
|
||||
onChange={(e) => {
|
||||
const next = [...fields];
|
||||
const t = e.target.value as FormField['type'];
|
||||
if (t === 'clickup_status') {
|
||||
const fieldType = e.target.value as FormField['type'];
|
||||
if (fieldType === 'clickup_status') {
|
||||
next[idx] = { name: f.name, label: f.label, type: 'clickup_status', statusOptions: f.statusOptions };
|
||||
} else {
|
||||
next[idx] = { name: f.name, label: f.label, type: t };
|
||||
next[idx] = { name: f.name, label: f.label, type: fieldType };
|
||||
}
|
||||
setFields(next);
|
||||
}}
|
||||
|
|
@ -96,7 +99,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
<option value="email">E-Mail</option>
|
||||
<option value="date">Datum</option>
|
||||
<option value="boolean">Ja/Nein</option>
|
||||
<option value="clickup_status">ClickUp-Status (Liste)</option>
|
||||
<option value="clickup_status">{t('formStartNodeConfig.clickupstatusListe')}</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -111,7 +114,7 @@ export const FormStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params,
|
|||
type="button"
|
||||
className={styles.startsAddBtn}
|
||||
onClick={() =>
|
||||
setFields([...fields, { name: `field${fields.length + 1}`, label: 'Neues Feld', type: 'text' }])
|
||||
setFields([...fields, { name: `field${fields.length + 1}`, label: t('formStartNodeConfig.newField'), type: 'text' }])
|
||||
}
|
||||
>
|
||||
+ Feld
|
||||
|
|
|
|||
|
|
@ -16,13 +16,17 @@ import {
|
|||
} from '../runtime/scheduleCron';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
const MODE_OPTIONS: { value: ScheduleMode; title: string; subtitle: string }[] = [
|
||||
{ value: 'daily', title: 'Täglich', subtitle: 'Jeden Tag zur gleichen Zeit' },
|
||||
{ value: 'weekdays', title: 'Werktage', subtitle: 'Montag bis Freitag' },
|
||||
{ value: 'weekly', title: 'Bestimmte Tage', subtitle: 'Wochentage auswählen' },
|
||||
{ value: 'calendar', title: 'Ein anderer Zeitraum', subtitle: 'Monatlich oder jährlich wiederkehrend' },
|
||||
{ value: 'interval', title: 'Intervall', subtitle: 'In regelmäßigen Abständen' },
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
function _getModeOptions(t: (key: string) => string): { value: ScheduleMode; title: string; subtitle: string }[] {
|
||||
return [
|
||||
{ value: 'daily', title: t('scheduleStartNodeConfig.taeglich'), subtitle: t('scheduleStartNodeConfig.jedenTagZurGleichenZeit') },
|
||||
{ value: 'weekdays', title: t('scheduleStartNodeConfig.werktage'), subtitle: t('scheduleStartNodeConfig.montagBisFreitag') },
|
||||
{ value: 'weekly', title: t('scheduleStartNodeConfig.bestimmteTage'), subtitle: t('scheduleStartNodeConfig.wochentageAuswaehlen') },
|
||||
{ value: 'calendar', title: t('scheduleStartNodeConfig.einAndererZeitraum'), subtitle: t('scheduleStartNodeConfig.monatlichOderJaehrlich') },
|
||||
{ value: 'interval', title: t('scheduleStartNodeConfig.intervall'), subtitle: t('scheduleStartNodeConfig.inRegelmaessigenAbstaenden') },
|
||||
];
|
||||
}
|
||||
|
||||
const MONTH_NAMES_DE = [
|
||||
'Januar',
|
||||
|
|
@ -39,13 +43,15 @@ const MONTH_NAMES_DE = [
|
|||
'Dezember',
|
||||
];
|
||||
|
||||
const INTERVAL_UNITS: { value: IntervalUnit; label: string; title: string }[] = [
|
||||
{ value: 'seconds', label: 'sek', title: 'Sekunden' },
|
||||
{ value: 'minutes', label: 'min', title: 'Minuten' },
|
||||
{ value: 'hours', label: 'h', title: 'Stunden' },
|
||||
{ value: 'days', label: 'd', title: 'Tage' },
|
||||
{ value: 'years', label: 'a', title: 'Jahre' },
|
||||
function _getIntervalUnits(t: (key: string) => string): { value: IntervalUnit; label: string; title: string }[] {
|
||||
return [
|
||||
{ value: 'seconds', label: t('scheduleStartNodeConfig.sek'), title: t('scheduleStartNodeConfig.sekunden') },
|
||||
{ value: 'minutes', label: t('scheduleStartNodeConfig.min'), title: t('scheduleStartNodeConfig.minuten') },
|
||||
{ value: 'hours', label: t('scheduleStartNodeConfig.h'), title: t('scheduleStartNodeConfig.stunden') },
|
||||
{ value: 'days', label: t('scheduleStartNodeConfig.d'), title: t('scheduleStartNodeConfig.tage') },
|
||||
{ value: 'years', label: t('scheduleStartNodeConfig.a'), title: t('scheduleStartNodeConfig.jahre') },
|
||||
];
|
||||
}
|
||||
|
||||
function timeString(hour: number, minute: number): string {
|
||||
return `${String(Math.max(0, Math.min(23, hour))).padStart(2, '0')}:${String(Math.max(0, Math.min(59, minute))).padStart(2, '0')}`;
|
||||
|
|
@ -77,6 +83,9 @@ function clampInterval(value: number, unit: IntervalUnit): number {
|
|||
const EASE_SMOOTH = [0.33, 1, 0.68, 1] as const;
|
||||
|
||||
export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||
const { t } = useLanguage();
|
||||
const modeOptions = _getModeOptions(t);
|
||||
const intervalUnits = _getIntervalUnits(t);
|
||||
const [spec, setSpec] = useState<ScheduleSpec>(() => scheduleSpecFromParams(params));
|
||||
const prefersReducedMotion = useReducedMotion();
|
||||
const specModeRef = useRef(spec.mode);
|
||||
|
|
@ -128,7 +137,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
const onModeCardPointerEvent = (
|
||||
phase: 'pointerdown' | 'click',
|
||||
e: React.PointerEvent | React.MouseEvent,
|
||||
o: (typeof MODE_OPTIONS)[number]
|
||||
o: { value: ScheduleMode; title: string; subtitle: string }
|
||||
) => {
|
||||
const el = e.target as HTMLElement;
|
||||
const cur = e.currentTarget as HTMLElement;
|
||||
|
|
@ -206,7 +215,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
|
||||
<LayoutGroup>
|
||||
<div className={styles.scheduleModeStack}>
|
||||
{MODE_OPTIONS.map((o) => (
|
||||
{modeOptions.map((o) => (
|
||||
<motion.div
|
||||
key={o.value}
|
||||
data-schedule-mode={o.value}
|
||||
|
|
@ -394,7 +403,7 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
|
||||
{o.value === 'interval' && (
|
||||
<div className={styles.scheduleIntervalRow}>
|
||||
<span className={styles.scheduleFieldLabel}>Alle</span>
|
||||
<span className={styles.scheduleFieldLabel}>{t('scheduleStartNodeConfig.alle')}</span>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
|
|
@ -411,9 +420,9 @@ export const ScheduleStartNodeConfig: React.FC<NodeConfigRendererProps> = ({ par
|
|||
className={styles.scheduleUnitSelect}
|
||||
value={spec.intervalUnit}
|
||||
onChange={(e) => setIntervalUnit(e.target.value as IntervalUnit)}
|
||||
title={INTERVAL_UNITS.find((u) => u.value === spec.intervalUnit)?.title}
|
||||
title={intervalUnits.find((u) => u.value === spec.intervalUnit)?.title}
|
||||
>
|
||||
{INTERVAL_UNITS.map((u) => (
|
||||
{intervalUnits.map((u) => (
|
||||
<option key={u.value} value={u.value} title={u.title}>
|
||||
{u.label}
|
||||
</option>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import { getMimeTypeOptionsFromUploadParams } from '../runtime/fileTypeMimeMappi
|
|||
import { operatorsForType } from '../shared/conditionOperators';
|
||||
import styles from '../../editor/Automation2FlowEditor.module.css';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export interface SwitchCase {
|
||||
operator: string;
|
||||
value?: string | number | boolean;
|
||||
|
|
@ -31,6 +33,7 @@ function normalizeCase(c: unknown): SwitchCase {
|
|||
}
|
||||
|
||||
export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, updateParam }) => {
|
||||
const { t } = useLanguage();
|
||||
const dataFlow = useAutomation2DataFlow();
|
||||
|
||||
const valueParam = params.value;
|
||||
|
|
@ -111,7 +114,7 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
onChange={(e) => handleCaseValueChange(index, e.target.value)}
|
||||
className={styles.startsInput}
|
||||
>
|
||||
<option value="">— MIME-Type wählen —</option>
|
||||
<option value="">{t('switchNodeConfig.mimetypeWaehlen')}</option>
|
||||
{mimeTypeOptions.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label} ({o.value})
|
||||
|
|
@ -151,9 +154,9 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
}}
|
||||
className={styles.startsInput}
|
||||
>
|
||||
<option value="">— wählen —</option>
|
||||
<option value="true">Ja / wahr</option>
|
||||
<option value="false">Nein / falsch</option>
|
||||
<option value="">{t('switchNodeConfig.waehlen')}</option>
|
||||
<option value="true">{t('switchNodeConfig.jaWahr')}</option>
|
||||
<option value="false">{t('switchNodeConfig.neinFalsch')}</option>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
|
@ -186,24 +189,24 @@ export const SwitchNodeConfig: React.FC<NodeConfigRendererProps> = ({ params, up
|
|||
<RefSourceSelect
|
||||
value={ref}
|
||||
onChange={handleRefChange}
|
||||
placeholder="Feld zum Vergleichen wählen…"
|
||||
placeholder={t('switchNodeConfig.feldZumVergleichenWaehlen')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!ref && (
|
||||
<div className={styles.ifElseConditionRow}>
|
||||
<label>Fester Wert (falls keine Referenz)</label>
|
||||
<label>{t('switchNodeConfig.festerWertFallsKeineReferenz')}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={String(staticValue ?? '')}
|
||||
onChange={(e) => handleStaticValueChange(e.target.value)}
|
||||
placeholder="z.B. CH oder 42"
|
||||
placeholder={t('switchNodeConfig.zbChOder42')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.ifElseConditionRow}>
|
||||
<label>Fälle (Reihenfolge = Ausgang)</label>
|
||||
<label>{t('switchNodeConfig.faelleReihenfolgeAusgang')}</label>
|
||||
<div className={styles.formFieldsList}>
|
||||
{cases.map((c, i) => {
|
||||
const opDef = operators.find((o) => o.value === c.operator) ?? operators[0];
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import { FaFolder, FaFolderOpen, FaPlus, FaPen, FaTrash, FaChevronRight, FaGlobe
|
|||
import { usePrompt, type PromptOptions } from '../../hooks/usePrompt';
|
||||
import styles from './FolderTree.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
/* ── Public types ──────────────────────────────────────────────────────── */
|
||||
|
||||
export interface FolderNode {
|
||||
|
|
@ -184,6 +186,7 @@ interface SelectionCtx {
|
|||
/* ── File node (leaf) ─────────────────────────────────────────────────── */
|
||||
|
||||
function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
|
||||
const { t } = useLanguage();
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
|
|
@ -267,7 +270,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
|
|||
</button>
|
||||
)}
|
||||
{sel.selectedFileIds.length > 0 && sel.onDeleteFiles && (
|
||||
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFiles} title={`${sel.selectedFileIds.length} Dateien löschen`}>
|
||||
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFiles} title={`${sel.selectedFileIds.length} ${t('folderTree.dateienLoeschen')}`}>
|
||||
<FaTrash />
|
||||
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFileIds.length}</span>
|
||||
</button>
|
||||
|
|
@ -275,7 +278,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
|
|||
</>
|
||||
) : (
|
||||
(sel.onDeleteFile || sel.onDeleteFiles) && (
|
||||
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteSingle} title="Löschen">
|
||||
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteSingle} title={t('folderTree.loeschen')}>
|
||||
<FaTrash />
|
||||
</button>
|
||||
)
|
||||
|
|
@ -308,7 +311,7 @@ function _FileItem({ file, sel }: { file: FileNode; sel: SelectionCtx }) {
|
|||
e.stopPropagation();
|
||||
sel.onNeutralizeToggle?.(file.id, !file.neutralize);
|
||||
}}
|
||||
title={file.neutralize ? 'Neutralisierung aktiv (klicken zum Deaktivieren)' : 'Neutralisierung aus (klicken zum Aktivieren)'}
|
||||
title={file.neutralize ? t('folderTree.neutralisierungAktivKlickenZumDeaktivieren') : t('folderTree.neutralisierungAusKlickenZumAktivieren')}
|
||||
style={{ fontSize: 14, opacity: file.neutralize ? 1 : 0.4 }}
|
||||
>
|
||||
{'\uD83D\uDD12'}
|
||||
|
|
@ -351,6 +354,7 @@ function _TreeNode({
|
|||
onCreateFolder, onRenameFolder, onDeleteFolder, onMoveFolder, onMoveFolders, onMoveFile, onMoveFiles,
|
||||
onDownloadFolder,
|
||||
}: TreeNodeProps) {
|
||||
const { t } = useLanguage();
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
const [renameValue, setRenameValue] = useState(node.name);
|
||||
const [dropOver, setDropOver] = useState(false);
|
||||
|
|
@ -377,7 +381,7 @@ function _TreeNode({
|
|||
const _handleAdd = useCallback(async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!onCreateFolder) return;
|
||||
const name = await promptFolderName('Neuer Ordnername:', { title: 'Neuer Ordner', placeholder: 'Ordnername' });
|
||||
const name = await promptFolderName('Neuer Ordnername:', { title: t('folderTree.newFolder'), placeholder: 'Ordnername' });
|
||||
if (name?.trim()) {
|
||||
await onCreateFolder(name.trim(), node.id);
|
||||
if (!expandedIds.has(node.id)) onToggle(node.id);
|
||||
|
|
@ -496,37 +500,37 @@ function _TreeNode({
|
|||
)}
|
||||
<span className={styles.actions}>
|
||||
{onDownloadFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
|
||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onDownloadFolder(node.id, node.name); }} title="Ordner herunterladen (ZIP)">
|
||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onDownloadFolder(node.id, node.name); }} title={t('folderTree.ordnerHerunterladenZip')}>
|
||||
<FaDownload />
|
||||
</button>
|
||||
)}
|
||||
{onCreateFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
|
||||
<button className={styles.actionBtn} onClick={_handleAdd} title="Neuer Unterordner">
|
||||
<button className={styles.actionBtn} onClick={_handleAdd} title={t('folderTree.neuerUnterordner')}>
|
||||
<FaPlus />
|
||||
</button>
|
||||
)}
|
||||
{onRenameFolder && !(isMultiSelected && sel.selectedItemIds.size > 1) && (
|
||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); setRenameValue(node.name); setRenaming(true); }} title="Umbenennen">
|
||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); setRenameValue(node.name); setRenaming(true); }} title={t('folderTree.umbenennen')}>
|
||||
<FaPen />
|
||||
</button>
|
||||
)}
|
||||
{isMultiSelected && sel.selectedItemIds.size > 1 ? (
|
||||
<>
|
||||
{sel.selectedFolderIds.length > 0 && sel.onDeleteFolders && (
|
||||
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFolders} title={`${sel.selectedFolderIds.length} Ordner löschen`}>
|
||||
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFolders} title={`${sel.selectedFolderIds.length} ${t('folderTree.ordnerLoeschen')}`}>
|
||||
<FaFolder style={{ fontSize: 8, marginRight: 1 }} /><FaTrash />
|
||||
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFolderIds.length}</span>
|
||||
</button>
|
||||
)}
|
||||
{sel.selectedFileIds.length > 0 && sel.onDeleteFiles && (
|
||||
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFiles} title={`${sel.selectedFileIds.length} Dateien löschen`}>
|
||||
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteFiles} title={`${sel.selectedFileIds.length} ${t('folderTree.dateienLoeschen')}`}>
|
||||
<FaTrash />
|
||||
<span style={{ fontSize: 9, marginLeft: 2, fontWeight: 600 }}>{sel.selectedFileIds.length}</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : onDeleteFolder && (
|
||||
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteSingle} title="Löschen">
|
||||
<button className={`${styles.actionBtn} ${styles.danger}`} onClick={_handleDeleteSingle} title={t('folderTree.loeschen')}>
|
||||
<FaTrash />
|
||||
</button>
|
||||
)}
|
||||
|
|
@ -576,6 +580,7 @@ export default function FolderTree({
|
|||
onRenameFile, onDeleteFile, onDeleteFiles, onDeleteFolders, onRefresh, onDownloadFolder,
|
||||
onScopeChange, onNeutralizeToggle,
|
||||
}: FolderTreeProps) {
|
||||
const { t } = useLanguage();
|
||||
const [internalExpandedIds, setInternalExpandedIds] = useState<Set<string>>(new Set());
|
||||
const [rootDropOver, setRootDropOver] = useState(false);
|
||||
const [internalSelectedIds, setInternalSelectedIds] = useState<Set<string>>(new Set());
|
||||
|
|
@ -751,7 +756,7 @@ export default function FolderTree({
|
|||
<span className={`${styles.folderName} ${styles.rootLabel}`}>(Global)</span>
|
||||
<span className={styles.rootActions}>
|
||||
{onRefresh && (
|
||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onRefresh(); }} title="Aktualisieren">
|
||||
<button className={styles.actionBtn} onClick={(e) => { e.stopPropagation(); onRefresh(); }} title={t('folderTree.aktualisieren')}>
|
||||
<FaSyncAlt />
|
||||
</button>
|
||||
)}
|
||||
|
|
@ -760,10 +765,10 @@ export default function FolderTree({
|
|||
className={styles.actionBtn}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
const name = await promptFolderName('Neuer Ordnername:', { title: 'Neuer Ordner', placeholder: 'Ordnername' });
|
||||
const name = await promptFolderName('Neuer Ordnername:', { title: t('folderTree.newFolder'), placeholder: 'Ordnername' });
|
||||
if (name?.trim()) await onCreateFolder(name.trim(), null);
|
||||
}}
|
||||
title="Neuer Ordner"
|
||||
title={t('folderTree.neuerOrdner')}
|
||||
>
|
||||
<FaPlus />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -455,6 +455,8 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
|
||||
// Validate all fields
|
||||
const validateFields = (): boolean => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const newErrors: Record<string, string> = {};
|
||||
const filteredAttrs = getFilteredAttributes();
|
||||
|
||||
|
|
@ -667,10 +669,10 @@ export function FormGeneratorForm<T extends Record<string, any>>({
|
|||
const multilingualValue = isTextMultilingual(value) ? value : { en: typeof value === 'string' ? value : '' };
|
||||
|
||||
const languages = [
|
||||
{ code: 'en', label: 'English', required: true },
|
||||
{ code: 'ge', label: 'German', required: false },
|
||||
{ code: 'fr', label: 'French', required: false },
|
||||
{ code: 'it', label: 'Italian', required: false }
|
||||
{ code: 'en', label: 'EN', required: true },
|
||||
{ code: 'ge', label: 'DE', required: false },
|
||||
{ code: 'fr', label: 'FR', required: false },
|
||||
{ code: 'it', label: 'IT', required: false }
|
||||
];
|
||||
|
||||
const handleMultilingualChange = (langCode: string, langValue: string) => {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import {
|
|||
PieChart, Pie, Cell, Legend
|
||||
} from 'recharts';
|
||||
import styles from './FormGeneratorReport.module.css';
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
import type {
|
||||
FormGeneratorReportProps,
|
||||
ReportSection,
|
||||
|
|
@ -114,8 +116,10 @@ const _renderKpiGrid = (section: ReportSectionKpi): React.ReactNode => {
|
|||
// --- Bar Chart (vertical) ---
|
||||
|
||||
const _renderBarChart = (section: ReportSectionBarChart, currencyCode: string): React.ReactNode => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (!section.data?.length) {
|
||||
return <div className={styles.noData}>Keine Daten</div>;
|
||||
return <div className={styles.noData}>{t('formGeneratorReport.keineDaten')}</div>;
|
||||
}
|
||||
|
||||
const chartData = section.data.map(d => ({
|
||||
|
|
@ -158,8 +162,10 @@ const _renderBarChart = (section: ReportSectionBarChart, currencyCode: string):
|
|||
// --- Horizontal Bar Chart ---
|
||||
|
||||
const _renderHorizontalBar = (section: ReportSectionHorizontalBar, currencyCode: string): React.ReactNode => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (!section.data?.length) {
|
||||
return <div className={styles.noData}>Keine Daten</div>;
|
||||
return <div className={styles.noData}>{t('formGeneratorReport.keineDaten')}</div>;
|
||||
}
|
||||
|
||||
const maxValue = Math.max(...section.data.map(d => d.value), 0.01);
|
||||
|
|
@ -189,8 +195,10 @@ const _renderHorizontalBar = (section: ReportSectionHorizontalBar, currencyCode:
|
|||
// --- Line Chart ---
|
||||
|
||||
const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string): React.ReactNode => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (!section.data?.length) {
|
||||
return <div className={styles.noData}>Keine Daten</div>;
|
||||
return <div className={styles.noData}>{t('formGeneratorReport.keineDaten')}</div>;
|
||||
}
|
||||
|
||||
const formatter = section.formatValue || ((v: number) => _defaultFormatCurrency(v, currencyCode));
|
||||
|
|
@ -235,8 +243,10 @@ const _renderLineChart = (section: ReportSectionLineChart, currencyCode: string)
|
|||
// --- Area Chart ---
|
||||
|
||||
const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string): React.ReactNode => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (!section.data?.length) {
|
||||
return <div className={styles.noData}>Keine Daten</div>;
|
||||
return <div className={styles.noData}>{t('formGeneratorReport.keineDaten')}</div>;
|
||||
}
|
||||
|
||||
const formatter = section.formatValue || ((v: number) => _defaultFormatCurrency(v, currencyCode));
|
||||
|
|
@ -281,8 +291,10 @@ const _renderAreaChart = (section: ReportSectionAreaChart, currencyCode: string)
|
|||
// --- Pie Chart ---
|
||||
|
||||
const _renderPieChart = (section: ReportSectionPieChart, currencyCode: string): React.ReactNode => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (!section.data?.length) {
|
||||
return <div className={styles.noData}>Keine Daten</div>;
|
||||
return <div className={styles.noData}>{t('formGeneratorReport.keineDaten')}</div>;
|
||||
}
|
||||
|
||||
const formatter = section.formatValue || ((v: number) => _defaultFormatCurrency(v, currencyCode));
|
||||
|
|
@ -339,10 +351,11 @@ interface ReportTableSectionProps {
|
|||
}
|
||||
|
||||
const _ReportTableSection: React.FC<ReportTableSectionProps> = ({ section, currencyCode }) => {
|
||||
const { t } = useLanguage();
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
if (!section.rows?.length) {
|
||||
return <div className={styles.noData}>Keine Daten</div>;
|
||||
return <div className={styles.noData}>{t('formGeneratorReport.keineDaten')}</div>;
|
||||
}
|
||||
|
||||
const maxRows = section.maxRows || 0;
|
||||
|
|
@ -440,6 +453,8 @@ const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode
|
|||
}
|
||||
|
||||
const _renderContent = (): React.ReactNode => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
switch (section.type) {
|
||||
case 'barChart':
|
||||
return _renderBarChart(section, currencyCode);
|
||||
|
|
@ -454,7 +469,7 @@ const _SectionWrapper: React.FC<SectionWrapperProps> = ({ section, currencyCode
|
|||
case 'table':
|
||||
return <_ReportTableSection section={section} currencyCode={currencyCode} />;
|
||||
default:
|
||||
return <div className={styles.noData}>Unbekannter Sektionstyp</div>;
|
||||
return <div className={styles.noData}>{t('formGeneratorReport.unbekannterSektionstyp')}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -482,6 +497,7 @@ interface ToolbarProps {
|
|||
const _Toolbar: React.FC<ToolbarProps> = ({
|
||||
periodSelector, dateRangeSelector, filters, filterState, onFilterStateChange
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const hasPeriod = !!periodSelector;
|
||||
const hasDateRange = dateRangeSelector?.enabled;
|
||||
const hasFilters = filters && filters.length > 0;
|
||||
|
|
@ -518,22 +534,18 @@ const _Toolbar: React.FC<ToolbarProps> = ({
|
|||
const currentYear = new Date().getFullYear();
|
||||
const yearOptions = Array.from({ length: 5 }, (_, i) => currentYear - i);
|
||||
|
||||
const monthOptions = [
|
||||
{ value: 1, label: 'Januar' }, { value: 2, label: 'Februar' },
|
||||
{ value: 3, label: 'März' }, { value: 4, label: 'April' },
|
||||
{ value: 5, label: 'Mai' }, { value: 6, label: 'Juni' },
|
||||
{ value: 7, label: 'Juli' }, { value: 8, label: 'August' },
|
||||
{ value: 9, label: 'September' }, { value: 10, label: 'Oktober' },
|
||||
{ value: 11, label: 'November' }, { value: 12, label: 'Dezember' }
|
||||
];
|
||||
const monthOptions = Array.from({ length: 12 }, (_, i) => ({
|
||||
value: i + 1,
|
||||
label: String(i + 1),
|
||||
}));
|
||||
|
||||
const _renderPeriodLabel = (p: ReportPeriod): string => {
|
||||
const labels: Record<ReportPeriod, string> = {
|
||||
day: 'Tagesansicht',
|
||||
week: 'Wochenansicht',
|
||||
month: 'Monatsansicht',
|
||||
quarter: 'Quartalsansicht',
|
||||
year: 'Jahresansicht'
|
||||
day: t('formGeneratorReport.tagesansicht'),
|
||||
week: t('formGeneratorReport.wochenansicht'),
|
||||
month: t('formGeneratorReport.monatsansicht'),
|
||||
quarter: t('formGeneratorReport.quartalsansicht'),
|
||||
year: t('formGeneratorReport.jahresansicht')
|
||||
};
|
||||
return labels[p] || p;
|
||||
};
|
||||
|
|
@ -543,7 +555,7 @@ const _Toolbar: React.FC<ToolbarProps> = ({
|
|||
{/* Period Selector */}
|
||||
{hasPeriod && (
|
||||
<div className={styles.toolbarGroup}>
|
||||
<span className={styles.toolbarLabel}>Zeitraum</span>
|
||||
<span className={styles.toolbarLabel}>{t('formGeneratorReport.zeitraum')}</span>
|
||||
<select
|
||||
className={styles.select}
|
||||
value={filterState.period || periodSelector!.defaultPeriod}
|
||||
|
|
@ -588,7 +600,7 @@ const _Toolbar: React.FC<ToolbarProps> = ({
|
|||
{/* Date Range */}
|
||||
{hasDateRange && (
|
||||
<div className={styles.toolbarGroup}>
|
||||
<span className={styles.toolbarLabel}>Von</span>
|
||||
<span className={styles.toolbarLabel}>{t('formGeneratorReport.von')}</span>
|
||||
<input
|
||||
type="date"
|
||||
className={styles.dateInput}
|
||||
|
|
@ -649,7 +661,7 @@ export const FormGeneratorReport: React.FC<FormGeneratorReportProps> = ({
|
|||
subtitle,
|
||||
sections,
|
||||
loading = false,
|
||||
noDataMessage = 'Keine Daten verfügbar',
|
||||
noDataMessage,
|
||||
periodSelector,
|
||||
dateRangeSelector,
|
||||
filters,
|
||||
|
|
@ -657,6 +669,7 @@ export const FormGeneratorReport: React.FC<FormGeneratorReportProps> = ({
|
|||
currencyCode = 'CHF',
|
||||
className
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
// Build initial filter state
|
||||
const initialFilterState = useMemo((): ReportFilterState => {
|
||||
const state: ReportFilterState = { filters: {} };
|
||||
|
|
@ -715,7 +728,7 @@ export const FormGeneratorReport: React.FC<FormGeneratorReportProps> = ({
|
|||
filterState={filterState}
|
||||
onFilterStateChange={_handleFilterStateChange}
|
||||
/>
|
||||
<div className={styles.loadingContainer}>Lade Daten...</div>
|
||||
<div className={styles.loadingContainer}>{t('formGeneratorReport.ladeDaten')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -737,7 +750,7 @@ export const FormGeneratorReport: React.FC<FormGeneratorReportProps> = ({
|
|||
filterState={filterState}
|
||||
onFilterStateChange={_handleFilterStateChange}
|
||||
/>
|
||||
<div className={styles.noData}>{noDataMessage}</div>
|
||||
<div className={styles.noData}>{noDataMessage || t('formGeneratorReport.keineDatenVerfuegbar')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import { NotificationBell } from '../NotificationBell';
|
|||
import { _isOnboardingHidden, _showOnboarding } from '../OnboardingAssistant';
|
||||
import styles from './UserSection.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
export const UserSection: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const { user, logout } = useCurrentUser();
|
||||
const navigate = useNavigate();
|
||||
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
||||
|
|
@ -139,7 +143,7 @@ export const UserSection: React.FC = () => {
|
|||
<div className={styles.modalOverlay} onClick={() => setShowLegalModal(false)}>
|
||||
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2>Rechtliche Hinweise</h2>
|
||||
<h2>{t('userSection.rechtlicheHinweise')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setShowLegalModal(false)}
|
||||
|
|
@ -149,25 +153,25 @@ export const UserSection: React.FC = () => {
|
|||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
<div className={styles.legalSection}>
|
||||
<h3>Datenverarbeitung und KI-Nutzung</h3>
|
||||
<h3>{t('userSection.datenverarbeitungUndKinutzung')}</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>
|
||||
<h4>{t('userSection.1EinwilligungZurDatenverarbeitung')}</h4>
|
||||
<p>{t('userSection.mitDerNutzungDieserAnwendung')}</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>
|
||||
<li>{t('userSection.sieAutorisierenDieErfassungVerarbeitung')}</li>
|
||||
<li>{t('userSection.nutzerdatenKoennenAnDrittanbieterVon')}</li>
|
||||
<li>{t('userSection.dieseEinwilligungErstrecktSichAuf')}</li>
|
||||
</ul>
|
||||
|
||||
<h4>2. Anerkennung der KI-Verarbeitungsrisiken</h4>
|
||||
<h4>{t('userSection.2AnerkennungDerKiverarbeitungsrisiken')}</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>
|
||||
<li>{t('userSection.kisystemeKoennenUnerwarteteOderUngenaue')}</li>
|
||||
<li>{t('userSection.kidiensteKoennenDatenGemaessIhren')}</li>
|
||||
<li>{t('userSection.trotzSicherheitsmassnahmenKoennenDatenAnfaellig')}</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>
|
||||
<h4>{t('userSection.3Haftungsausschluss')}</h4>
|
||||
<p>{t('userSection.imGroesstmoeglichenUmfangVerzichtenSie')}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.legalLinks}>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { FaBell, FaCheck, FaTimes, FaEnvelope, FaCog, FaExclamationTriangle, FaC
|
|||
import { useNotifications, UserNotification } from '../../hooks/useNotifications';
|
||||
import styles from './NotificationBell.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
// Icon mapping for notification types
|
||||
const typeIcons: Record<string, React.ReactNode> = {
|
||||
invitation: <FaEnvelope />,
|
||||
|
|
@ -43,6 +45,7 @@ interface NotificationBellProps {
|
|||
}
|
||||
|
||||
export const NotificationBell: React.FC<NotificationBellProps> = ({ className }) => {
|
||||
const { t } = useLanguage();
|
||||
const {
|
||||
notifications,
|
||||
unreadCount,
|
||||
|
|
@ -156,7 +159,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
|
|||
<div className={styles.dropdown}>
|
||||
{/* Header */}
|
||||
<div className={styles.header}>
|
||||
<h3>Benachrichtigungen</h3>
|
||||
<h3>{t('notificationBell.benachrichtigungen')}</h3>
|
||||
{visibleNotifications.some(n => n.status === 'unread') && (
|
||||
<button
|
||||
className={styles.markAllRead}
|
||||
|
|
@ -170,7 +173,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
|
|||
{/* Content */}
|
||||
<div className={styles.content}>
|
||||
{loading && visibleNotifications.length === 0 && (
|
||||
<div className={styles.loading}>Lade...</div>
|
||||
<div className={styles.loading}>{t('notificationBell.lade')}</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
|
|
@ -180,7 +183,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
|
|||
{!loading && !error && visibleNotifications.length === 0 && (
|
||||
<div className={styles.empty}>
|
||||
<FaBell className={styles.emptyIcon} />
|
||||
<p>Keine Benachrichtigungen</p>
|
||||
<p>{t('notificationBell.keineBenachrichtigungen')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -250,7 +253,7 @@ export const NotificationBell: React.FC<NotificationBellProps> = ({ className })
|
|||
<button
|
||||
className={styles.dismissButton}
|
||||
onClick={(e) => handleDismiss(notification, e)}
|
||||
aria-label="Schliessen"
|
||||
aria-label={t('notificationBell.schliessen')}
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
|||
import api from '../api';
|
||||
import OnboardingWizard from './OnboardingWizard';
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
interface OnboardingStep {
|
||||
id: string;
|
||||
label: string;
|
||||
|
|
@ -45,6 +47,7 @@ function _hideOnboarding(): void {
|
|||
}
|
||||
|
||||
const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss }) => {
|
||||
const { t } = useLanguage();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [hidden, setHidden] = useState(() => _isOnboardingHidden());
|
||||
|
|
@ -94,22 +97,22 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
|||
|
||||
onboardingSteps.push({
|
||||
id: 'mandate',
|
||||
label: 'Mandant einrichten',
|
||||
label: t('onboardingAssistant.setupMandate'),
|
||||
description: hasAdminMandate
|
||||
? 'Dein Mandant ist eingerichtet.'
|
||||
: hasFeature
|
||||
? 'Du bist Mitglied eines Mandanten.'
|
||||
: 'Erstelle deinen Arbeitsbereich.',
|
||||
? t('onboardingAssistant.duBistMitgliedEinesMandanten')
|
||||
: t('onboardingAssistant.erstelleDeinenArbeitsbereich'),
|
||||
completed: mandateStepDone,
|
||||
action: mandateStepDone ? undefined : () => setShowWizard(true),
|
||||
});
|
||||
|
||||
onboardingSteps.push({
|
||||
id: 'feature',
|
||||
label: 'Erstes Feature aktivieren',
|
||||
label: t('onboardingAssistant.activateFirstFeature'),
|
||||
description: hasFeature
|
||||
? 'Du hast aktive Features.'
|
||||
: 'Aktiviere dein erstes Feature im Store.',
|
||||
? t('onboardingAssistant.duHastAktiveFeatures')
|
||||
: t('onboardingAssistant.aktiviereDeinErstesFeatureIm'),
|
||||
completed: hasFeature,
|
||||
action: hasFeature ? undefined : () => navigate('/store'),
|
||||
});
|
||||
|
|
@ -123,10 +126,10 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
|||
|
||||
onboardingSteps.push({
|
||||
id: 'connection',
|
||||
label: 'Erste Datenquelle einbinden',
|
||||
label: t('onboardingAssistant.connectFirstDataSource'),
|
||||
description: hasConnection
|
||||
? 'Du hast Verbindungen eingerichtet.'
|
||||
: 'Verbinde deine erste Datenquelle.',
|
||||
? t('onboardingAssistant.duHastVerbindungenEingerichtet')
|
||||
: t('onboardingAssistant.verbindeDeineErsteDatenquelle'),
|
||||
completed: hasConnection,
|
||||
action: hasConnection ? undefined : () => navigate('/basedata/connections'),
|
||||
});
|
||||
|
|
@ -144,10 +147,10 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
|||
const chatAction = workspaceInstancePath ? () => navigate(workspaceInstancePath!) : undefined;
|
||||
onboardingSteps.push({
|
||||
id: 'chat',
|
||||
label: 'Ersten AI-Chat starten',
|
||||
label: t('onboardingAssistant.startFirstAiChat'),
|
||||
description: hasChat
|
||||
? 'Du hast bereits Chats gestartet.'
|
||||
: 'Starte deinen ersten Chat mit dem AI-Assistenten.',
|
||||
? t('onboardingAssistant.duHastBereitsChatsGestartet')
|
||||
: t('onboardingAssistant.starteDeinenErstenChatMit'),
|
||||
completed: hasChat,
|
||||
action: hasChat ? undefined : chatAction,
|
||||
});
|
||||
|
|
@ -210,7 +213,7 @@ const OnboardingAssistant: React.FC<OnboardingAssistantProps> = ({ onDismiss })
|
|||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
|
||||
<div>
|
||||
<h3 style={{ margin: 0, fontSize: '1rem' }}>Willkommen bei PowerOn</h3>
|
||||
<h3 style={{ margin: 0, fontSize: '1rem' }}>{t('onboardingAssistant.willkommenBeiPoweron')}</h3>
|
||||
<p style={{ margin: '4px 0 0', fontSize: '0.85rem', color: 'var(--text-secondary, #6b7280)' }}>
|
||||
{completedCount} von {steps.length} Schritten abgeschlossen
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import React, { useState } from 'react';
|
||||
import api from '../api';
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
interface OnboardingWizardProps {
|
||||
onComplete: () => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismiss }) => {
|
||||
const { t } = useLanguage();
|
||||
const [planKey, setPlanKey] = useState<'TRIAL_7D' | 'STANDARD_MONTHLY'>('TRIAL_7D');
|
||||
const [mandateName, setMandateName] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -21,7 +24,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
|||
companyName: mandateName.trim() || undefined,
|
||||
});
|
||||
if (res.data?.alreadyProvisioned) {
|
||||
setError('Du hast bereits einen Mandanten mit Admin-Zugang.');
|
||||
setError(t('onboardingWizard.duHastBereitsEinenMandanten'));
|
||||
return;
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('features-changed'));
|
||||
|
|
@ -43,7 +46,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
|||
background: 'var(--bg-primary, #fff)', borderRadius: '12px', padding: '32px',
|
||||
maxWidth: '480px', width: '90%', boxShadow: '0 8px 32px rgba(0,0,0,0.2)',
|
||||
}}>
|
||||
<h2 style={{ margin: '0 0 8px', fontSize: '1.5rem' }}>Mandant erstellen</h2>
|
||||
<h2 style={{ margin: '0 0 8px', fontSize: '1.5rem' }}>{t('onboardingWizard.mandantErstellen')}</h2>
|
||||
<p style={{ color: 'var(--text-secondary, #666)', margin: '0 0 24px' }}>
|
||||
Erstelle deinen eigenen Arbeitsbereich mit Abo-Auswahl.
|
||||
</p>
|
||||
|
|
@ -57,7 +60,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
|||
<input type="radio" name="plan" checked={planKey === 'TRIAL_7D'}
|
||||
onChange={() => setPlanKey('TRIAL_7D')} />
|
||||
<div>
|
||||
<strong>Kostenlos testen</strong>
|
||||
<strong>{t('onboardingWizard.kostenlosTesten')}</strong>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
|
||||
7 Tage gratis, danach flexibel upgraden
|
||||
</div>
|
||||
|
|
@ -72,7 +75,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
|||
<input type="radio" name="plan" checked={planKey === 'STANDARD_MONTHLY'}
|
||||
onChange={() => setPlanKey('STANDARD_MONTHLY')} />
|
||||
<div>
|
||||
<strong>Standard (Monatlich)</strong>
|
||||
<strong>{t('onboardingWizard.standardMonatlich')}</strong>
|
||||
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary, #666)' }}>
|
||||
Team-Workspace mit vollem Funktionsumfang
|
||||
</div>
|
||||
|
|
@ -87,7 +90,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
|||
<input
|
||||
type="text" value={mandateName}
|
||||
onChange={(e) => setMandateName(e.target.value)}
|
||||
placeholder="z. B. Firmenname oder Projektname"
|
||||
placeholder={t('onboardingWizard.zBFirmennameOderProjektname')}
|
||||
style={{
|
||||
width: '100%', padding: '10px 12px', borderRadius: '6px',
|
||||
border: '1px solid var(--border, #d1d5db)', fontSize: '1rem',
|
||||
|
|
@ -111,7 +114,7 @@ const OnboardingWizard: React.FC<OnboardingWizardProps> = ({ onComplete, onDismi
|
|||
background: 'var(--accent, #4f46e5)', color: '#fff', cursor: 'pointer',
|
||||
opacity: loading ? 0.6 : 1,
|
||||
}}>
|
||||
{loading ? 'Wird eingerichtet...' : 'Mandant erstellen'}
|
||||
{loading ? t('onboardingWizard.wirdEingerichtet') : t('onboardingWizard.mandantErstellen')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import React, { useEffect, useMemo, useState, useRef, useCallback } from 'react'
|
|||
import { useBilling } from '../../hooks/useBilling';
|
||||
import styles from './ProviderSelector.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES & HELPERS
|
||||
// ============================================================================
|
||||
|
|
@ -109,14 +111,14 @@ interface ProviderSelectProps {
|
|||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
export const ProviderSelect: React.FC<ProviderSelectProps> = ({
|
||||
value,
|
||||
export const ProviderSelect: React.FC<ProviderSelectProps> = ({ value,
|
||||
onChange,
|
||||
disabled = false,
|
||||
className,
|
||||
label = 'AI-Provider',
|
||||
showLabel = true,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { allowedProviders, loadAllowedProviders, loading } = useBilling();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -141,7 +143,7 @@ export const ProviderSelect: React.FC<ProviderSelectProps> = ({
|
|||
disabled={disabled || loading}
|
||||
className={styles.select}
|
||||
>
|
||||
<option value="">-- Auto --</option>
|
||||
<option value="">{t('providerSelector.auto')}</option>
|
||||
{providerOptions.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
|
|
@ -177,6 +179,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
defaultExpanded = false,
|
||||
excludeByDefault = [],
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||
const [initialExcludeApplied, setInitialExcludeApplied] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -279,7 +282,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
className={styles.triggerButton}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
disabled={disabled}
|
||||
title="Provider auswählen"
|
||||
title={t('providerSelector.providerAuswaehlen')}
|
||||
>
|
||||
<span className={styles.buttonIcon}>{summaryIcon}</span>
|
||||
</button>
|
||||
|
|
@ -300,7 +303,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className={styles.loading}>Lade...</div>
|
||||
<div className={styles.loading}>{t('providerSelector.lade')}</div>
|
||||
) : (
|
||||
<div className={styles.checkboxList}>
|
||||
{allowedProviders.map((provider) => (
|
||||
|
|
@ -343,8 +346,9 @@ export const ProviderBadges: React.FC<ProviderBadgesProps> = ({
|
|||
providers,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
if (providers.length === 0) {
|
||||
return <span className={styles.allProviders}>Alle Provider</span>;
|
||||
return <span className={styles.allProviders}>{t('providerSelector.alleProvider')}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import {
|
|||
} from '../../hooks/useRbacExportImport';
|
||||
import styles from './RbacExportImport.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
|
@ -41,26 +43,28 @@ interface RbacExportImportProps {
|
|||
// IMPORT MODE OPTIONS
|
||||
// =============================================================================
|
||||
|
||||
const IMPORT_MODES: { value: ImportMode; label: string; description: string; icon: React.ReactNode }[] = [
|
||||
function _getImportModes(t: (key: string) => string): { value: ImportMode; label: string; description: string; icon: React.ReactNode }[] {
|
||||
return [
|
||||
{
|
||||
value: 'merge',
|
||||
label: 'Zusammenführen',
|
||||
description: 'Bestehende Regeln aktualisieren, neue hinzufügen',
|
||||
label: t('rbacExportImport.zusammenfuehren'),
|
||||
description: t('rbacExportImport.bestehendeRegelnAktualisieren'),
|
||||
icon: <FaCheckCircle style={{ color: '#38a169' }} />,
|
||||
},
|
||||
{
|
||||
value: 'add_only',
|
||||
label: 'Nur hinzufügen',
|
||||
description: 'Nur neue Regeln hinzufügen, bestehende nicht ändern',
|
||||
label: t('rbacExportImport.nurHinzufuegen'),
|
||||
description: t('rbacExportImport.nurNeueRegelnHinzufuegen'),
|
||||
icon: <FaInfoCircle style={{ color: '#3182ce' }} />,
|
||||
},
|
||||
{
|
||||
value: 'replace',
|
||||
label: 'Ersetzen',
|
||||
description: 'Alle bestehenden Regeln löschen und ersetzen',
|
||||
label: t('rbacExportImport.ersetzen'),
|
||||
description: t('rbacExportImport.alleBestehendenRegelnLoeschen'),
|
||||
icon: <FaExclamationTriangle style={{ color: '#d69e2e' }} />,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PREVIEW COMPONENT
|
||||
|
|
@ -72,6 +76,7 @@ interface PreviewProps {
|
|||
}
|
||||
|
||||
const ExportPreview: React.FC<PreviewProps> = ({ data, onClose }) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className={styles.preview}>
|
||||
<div className={styles.previewHeader}>
|
||||
|
|
@ -83,7 +88,7 @@ const ExportPreview: React.FC<PreviewProps> = ({ data, onClose }) => {
|
|||
<h5>Scope</h5>
|
||||
<ul className={styles.previewList}>
|
||||
<li><strong>Typ:</strong> {data.scope.type}</li>
|
||||
{data.scope.mandateName && <li><strong>Mandant:</strong> {data.scope.mandateName}</li>}
|
||||
{data.scope.mandateName && <li><strong>{t('rbacExportImport.mandant')}</strong> {data.scope.mandateName}</li>}
|
||||
{data.scope.featureCode && <li><strong>Feature:</strong> {data.scope.featureCode}</li>}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -130,6 +135,8 @@ interface ImportResultProps {
|
|||
}
|
||||
|
||||
const ImportResult: React.FC<ImportResultProps> = ({ result, onClose }) => {
|
||||
const { t } = useLanguage();
|
||||
const importModes = _getImportModes(t);
|
||||
const isSuccess = result.status === 'success';
|
||||
|
||||
return (
|
||||
|
|
@ -141,21 +148,21 @@ const ImportResult: React.FC<ImportResultProps> = ({ result, onClose }) => {
|
|||
<FaExclamationTriangle className={styles.resultIcon} />
|
||||
)}
|
||||
<h4 className={styles.resultTitle}>
|
||||
{isSuccess ? 'Import erfolgreich' : 'Import fehlgeschlagen'}
|
||||
{isSuccess ? t('rbacExportImport.importErfolgreich') : t('rbacExportImport.importFehlgeschlagen')}
|
||||
</h4>
|
||||
<button className={styles.closeButton} onClick={onClose}>✕</button>
|
||||
</div>
|
||||
<div className={styles.resultContent}>
|
||||
<ul className={styles.resultStats}>
|
||||
<li><strong>Modus:</strong> {IMPORT_MODES.find(m => m.value === result.mode)?.label}</li>
|
||||
<li><strong>Rollen erstellt:</strong> {result.rolesCreated}</li>
|
||||
<li><strong>Rollen aktualisiert:</strong> {result.rolesUpdated}</li>
|
||||
<li><strong>Regeln erstellt:</strong> {result.rulesCreated}</li>
|
||||
<li><strong>Regeln aktualisiert:</strong> {result.rulesUpdated}</li>
|
||||
<li><strong>Modus:</strong> {importModes.find(m => m.value === result.mode)?.label}</li>
|
||||
<li><strong>{t('rbacExportImport.rollenErstellt')}</strong> {result.rolesCreated}</li>
|
||||
<li><strong>{t('rbacExportImport.rollenAktualisiert')}</strong> {result.rolesUpdated}</li>
|
||||
<li><strong>{t('rbacExportImport.regelnErstellt')}</strong> {result.rulesCreated}</li>
|
||||
<li><strong>{t('rbacExportImport.regelnAktualisiert')}</strong> {result.rulesUpdated}</li>
|
||||
</ul>
|
||||
{result.errors && result.errors.length > 0 && (
|
||||
<div className={styles.resultErrors}>
|
||||
<h5>Fehler:</h5>
|
||||
<h5>{t('rbacExportImport.fehler')}</h5>
|
||||
<ul>
|
||||
{result.errors.map((err, i) => (
|
||||
<li key={i}>{err}</li>
|
||||
|
|
@ -178,6 +185,8 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
isGlobal = false,
|
||||
featureCode,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const importModes = _getImportModes(t);
|
||||
const {
|
||||
exporting,
|
||||
importing,
|
||||
|
|
@ -332,7 +341,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
) : (
|
||||
<>
|
||||
<FaUpload className={styles.fileIcon} />
|
||||
<span>JSON-Datei auswählen oder hier ablegen</span>
|
||||
<span>{t('rbacExportImport.jsondateiAuswaehlenOderHierAblegen')}</span>
|
||||
</>
|
||||
)}
|
||||
</label>
|
||||
|
|
@ -340,7 +349,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
<button
|
||||
className={styles.clearButton}
|
||||
onClick={handleClearImport}
|
||||
title="Datei entfernen"
|
||||
title={t('rbacExportImport.dateiEntfernen')}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
|
|
@ -358,7 +367,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
{importData && (
|
||||
<div className={styles.importInfo}>
|
||||
<div className={styles.importStats}>
|
||||
<span><strong>Rollen:</strong> {importData.roles.length}</span>
|
||||
<span><strong>{t('rbacExportImport.rollen')}</strong> {importData.roles.length}</span>
|
||||
<span><strong>Regeln:</strong> {importData.accessRules.length}</span>
|
||||
<span><strong>Quelle:</strong> {importData.scope.type}</span>
|
||||
</div>
|
||||
|
|
@ -376,7 +385,7 @@ export const RbacExportImport: React.FC<RbacExportImportProps> = ({
|
|||
<div className={styles.importModeSection}>
|
||||
<h4 className={styles.importModeTitle}>Import-Modus</h4>
|
||||
<div className={styles.importModes}>
|
||||
{IMPORT_MODES.map(mode => (
|
||||
{importModes.map(mode => (
|
||||
<label
|
||||
key={mode.value}
|
||||
className={`${styles.importModeOption} ${importMode === mode.value ? styles.selected : ''}`}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { BaseTextFieldProps } from '../TextField/TextFieldTypes';
|
|||
import { autocompleteAddress, AddressSuggestion } from '../../../api/realEstateApi';
|
||||
import styles from './AddressAutocomplete.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
interface AddressAutocompleteProps extends BaseTextFieldProps {
|
||||
onSelect?: (suggestion: AddressSuggestion) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
|
||||
|
|
@ -12,8 +14,7 @@ interface AddressAutocompleteProps extends BaseTextFieldProps {
|
|||
maxSuggestions?: number;
|
||||
}
|
||||
|
||||
const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
|
||||
value = '',
|
||||
const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({ value = '',
|
||||
onChange,
|
||||
onSelect,
|
||||
placeholder,
|
||||
|
|
@ -34,6 +35,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
|
|||
maxSuggestions = 10,
|
||||
...props
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [suggestions, setSuggestions] = useState<AddressSuggestion[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
|
|
@ -274,7 +276,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
|
|||
<ul ref={suggestionsRef} className={styles.suggestionsList}>
|
||||
{isLoading && (
|
||||
<li className={styles.suggestionItem}>
|
||||
<span className={styles.loadingText}>Suche Adressen...</span>
|
||||
<span className={styles.loadingText}>{t('addressAutocomplete.sucheAdressen')}</span>
|
||||
</li>
|
||||
)}
|
||||
{!isLoading && autocompleteError && (
|
||||
|
|
@ -284,7 +286,7 @@ const AddressAutocomplete: React.FC<AddressAutocompleteProps> = ({
|
|||
)}
|
||||
{!isLoading && !autocompleteError && suggestions.length === 0 && query.length >= minQueryLength && (
|
||||
<li className={styles.suggestionItem}>
|
||||
<span className={styles.noResultsText}>Keine Adressen gefunden</span>
|
||||
<span className={styles.noResultsText}>{t('addressAutocomplete.keineAdressenGefunden')}</span>
|
||||
</li>
|
||||
)}
|
||||
{!isLoading && suggestions.map((suggestion, index) => (
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import styles from './AutoScroll.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
export interface AutoScrollProps {
|
||||
/**
|
||||
* Children to render inside the scrollable container
|
||||
|
|
@ -30,12 +32,12 @@ export interface AutoScrollProps {
|
|||
* AutoScroll component that automatically scrolls to the bottom when new content is added,
|
||||
* unless the user has scrolled up. Shows a button when user has scrolled up.
|
||||
*/
|
||||
const AutoScroll: React.FC<AutoScrollProps> = ({
|
||||
children,
|
||||
const AutoScroll: React.FC<AutoScrollProps> = ({ children,
|
||||
className = '',
|
||||
scrollDependency,
|
||||
threshold = 100
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [showNewMessageButton, setShowNewMessageButton] = useState(false);
|
||||
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
||||
|
|
@ -148,7 +150,7 @@ const AutoScroll: React.FC<AutoScrollProps> = ({
|
|||
<button
|
||||
className={styles.scrollToBottomButton}
|
||||
onClick={handleNewMessageClick}
|
||||
aria-label="Scroll to bottom"
|
||||
aria-label={t('autoScroll.scrollToBottom')}
|
||||
>
|
||||
<span className={styles.scrollArrow}>↓</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import React, { useState } from 'react';
|
|||
import { FaChevronDown, FaChevronUp, FaFilePdf, FaRuler } from 'react-icons/fa';
|
||||
import styles from './BauvorschriftenSection.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
export interface BauvorschriftenZone {
|
||||
zonenbezeichnung: string;
|
||||
ausnuetzungsziffer?: number;
|
||||
|
|
@ -20,6 +22,7 @@ export interface BauvorschriftenSectionProps {
|
|||
}
|
||||
|
||||
export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({ bauvorschriften }) => {
|
||||
const { t } = useLanguage();
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
|
||||
return (
|
||||
|
|
@ -39,7 +42,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
|||
<div className={styles.bauvorschriftenGrid}>
|
||||
{bauvorschriften.ausnuetzungsziffer !== undefined && bauvorschriften.ausnuetzungsziffer !== null && (
|
||||
<div className={styles.bauvorschriftItem}>
|
||||
<span className={styles.label}>Ausnützungsziffer:</span>
|
||||
<span className={styles.label}>{t('bauvorschriftenSection.ausnuetzungsziffer')}</span>
|
||||
<span className={styles.value}>{bauvorschriften.ausnuetzungsziffer}%</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -51,7 +54,7 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
|||
)}
|
||||
{bauvorschriften.gebaeudelaengeMax !== undefined && bauvorschriften.gebaeudelaengeMax !== null && (
|
||||
<div className={styles.bauvorschriftItem}>
|
||||
<span className={styles.label}>Gebäudelänge max:</span>
|
||||
<span className={styles.label}>{t('bauvorschriftenSection.gebaeudelaengeMax')}</span>
|
||||
<span className={styles.value}>{bauvorschriften.gebaeudelaengeMax} m</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -63,19 +66,19 @@ export const BauvorschriftenSection: React.FC<BauvorschriftenSectionProps> = ({
|
|||
)}
|
||||
{bauvorschriften.mehrlaengenzuschlag && (
|
||||
<div className={styles.bauvorschriftItem}>
|
||||
<span className={styles.label}>Mehrlängenzuschlag:</span>
|
||||
<span className={styles.label}>{t('bauvorschriftenSection.mehrlaengenzuschlag')}</span>
|
||||
<span className={styles.value}>{bauvorschriften.mehrlaengenzuschlag}</span>
|
||||
</div>
|
||||
)}
|
||||
{bauvorschriften.hoechstmassMax !== undefined && bauvorschriften.hoechstmassMax !== null && (
|
||||
<div className={styles.bauvorschriftItem}>
|
||||
<span className={styles.label}>Höchstmass max:</span>
|
||||
<span className={styles.label}>{t('bauvorschriftenSection.hoechstmassMax')}</span>
|
||||
<span className={styles.value}>{bauvorschriften.hoechstmassMax} m</span>
|
||||
</div>
|
||||
)}
|
||||
{bauvorschriften.fassadenhoehe && (
|
||||
<div className={styles.bauvorschriftItem}>
|
||||
<span className={styles.label}>Fassadenhöhe:</span>
|
||||
<span className={styles.label}>{t('bauvorschriftenSection.fassadenhoehe')}</span>
|
||||
<span className={styles.value}>{bauvorschriften.fassadenhoehe}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import { WorkflowFile } from '../../../hooks/usePlayground';
|
|||
import styles from './ConnectedFilesList.module.css';
|
||||
import { formatBinaryDataSizeBytes } from '../../../utils/formatDataSize';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
export interface ConnectedFilesListActionButton {
|
||||
type: 'edit' | 'delete' | 'download' | 'view' | 'copy' | 'connect' | 'play' | 'remove';
|
||||
onAction?: (file: WorkflowFile) => Promise<void> | void;
|
||||
|
|
@ -67,6 +69,7 @@ export function ConnectedFilesList({
|
|||
workflowId: _workflowId,
|
||||
emptyMessage = 'No files connected to this workflow'
|
||||
}: ConnectedFilesListProps) {
|
||||
const { t } = useLanguage();
|
||||
// Combine workflow files and pending files, deduplicating by fileId
|
||||
const allFiles = useMemo(() => {
|
||||
const fileMap = new Map<string, WorkflowFile>();
|
||||
|
|
@ -181,7 +184,7 @@ export function ConnectedFilesList({
|
|||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
<h3 className={styles.title}>Connected Files</h3>
|
||||
<h3 className={styles.title}>{t('connectedFilesList.connectedFiles')}</h3>
|
||||
<span className={styles.count}>({allFiles.length})</span>
|
||||
</div>
|
||||
<div className={styles.fileList}>
|
||||
|
|
@ -228,14 +231,14 @@ export function ConnectedFilesList({
|
|||
cursor: onAttach ? 'pointer' : 'default',
|
||||
userSelect: 'none' // Prevent text selection on click
|
||||
}}
|
||||
title={onAttach ? (isPendingFile ? 'Click to detach from next message' : 'Click to attach for next message') : undefined}
|
||||
title={onAttach ? (isPendingFile ? t('connectedFilesList.clickToDetachFromNext') : t('connectedFilesList.clickToAttachForNext')) : undefined}
|
||||
>
|
||||
<div className={styles.fileInfo}>
|
||||
<div className={styles.fileName} title={file.fileName}>
|
||||
{file.fileName}
|
||||
{onAttach && (
|
||||
<span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', color: '#666' }}>
|
||||
{isPendingFile ? '(click to detach)' : '(click to attach)'}
|
||||
{isPendingFile ? t('connectedFilesList.clickToDetach') : t('connectedFilesList.clickToAttach')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -245,7 +248,7 @@ export function ConnectedFilesList({
|
|||
</span>
|
||||
{file.source && (
|
||||
<span className={styles.fileSource}>
|
||||
{file.source === 'user_uploaded' ? 'Uploaded' : 'AI Created'}
|
||||
{file.source === 'user_uploaded' ? t('connectedFilesList.uploaded') : t('connectedFilesList.aiCreated')}
|
||||
</span>
|
||||
)}
|
||||
{isPendingFile && (
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ const TILE_LAYER_MINIMAL = L.tileLayer(
|
|||
import icon from 'leaflet/dist/images/marker-icon.png';
|
||||
import iconShadow from 'leaflet/dist/images/marker-shadow.png';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
const DefaultIcon = L.icon({
|
||||
iconUrl: icon,
|
||||
shadowUrl: iconShadow,
|
||||
|
|
@ -38,8 +40,7 @@ L.Marker.prototype.options.icon = DefaultIcon;
|
|||
|
||||
const SELECTED_STYLE = { color: '#3b82f6', weight: 3, fillColor: '#3b82f6', fillOpacity: 0.3 };
|
||||
|
||||
const MapViewLeaflet: React.FC<MapViewProps> = ({
|
||||
parcels = [],
|
||||
const MapViewLeaflet: React.FC<MapViewProps> = ({ parcels = [],
|
||||
combinedOutline,
|
||||
center,
|
||||
zoomBounds,
|
||||
|
|
@ -51,6 +52,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({
|
|||
showWfsParcels = false,
|
||||
parcelsApiBaseUrl = ''
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const mapRef = useRef<L.Map | null>(null);
|
||||
const mapContainerRef = useRef<HTMLDivElement>(null);
|
||||
const layersRef = useRef<L.Layer[]>([]);
|
||||
|
|
@ -192,7 +194,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({
|
|||
if (ring && ring.length >= 3) {
|
||||
const latLngs = ring.map(([x, y]) => toWgs84(x, y));
|
||||
const polygon = L.polygon(latLngs, SELECTED_STYLE);
|
||||
polygon.bindPopup('<div><strong>Ausgewählte Fläche</strong><br/><em>Zum Entfernen Parzelle im Panel nutzen</em></div>');
|
||||
polygon.bindPopup(`<div><strong>${t('mapViewLeaflet.ausgewaehlteFlaeche')}</strong><br/><em>${t('mapViewLeaflet.zumEntfernenParzelleImPanel')}</em></div>`);
|
||||
if (onParcelClick) {
|
||||
polygon.on('click', () => onParcelClick('combined'));
|
||||
}
|
||||
|
|
@ -205,7 +207,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({
|
|||
if (ring && ring.length >= 3) {
|
||||
const latLngs = ring.map(([x, y]) => toWgs84(x, y));
|
||||
const polygon = L.polygon(latLngs, SELECTED_STYLE);
|
||||
polygon.bindPopup('<div><strong>Ausgewählte Fläche</strong><br/><em>Zum Entfernen Parzelle im Panel nutzen</em></div>');
|
||||
polygon.bindPopup(`<div><strong>${t('mapViewLeaflet.ausgewaehlteFlaeche')}</strong><br/><em>${t('mapViewLeaflet.zumEntfernenParzelleImPanel')}</em></div>`);
|
||||
if (onParcelClick) {
|
||||
polygon.on('click', () => onParcelClick('combined'));
|
||||
}
|
||||
|
|
@ -223,7 +225,7 @@ const MapViewLeaflet: React.FC<MapViewProps> = ({
|
|||
<div>
|
||||
<strong>Parzelle ${parcel.number || parcel.id}</strong><br/>
|
||||
${parcel.egrid ? `EGRID: ${parcel.egrid}<br/>` : ''}
|
||||
<em>Ausgewählt</em>
|
||||
<em>{t('mapViewLeaflet.ausgewaehlt')}</em>
|
||||
</div>
|
||||
`);
|
||||
if (onParcelClick) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import { DocumentItem, ActionInfo } from '../MessageParts';
|
|||
import { WorkflowFile } from '../../../../hooks/usePlayground';
|
||||
import styles from '../Messages.module.css';
|
||||
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
|
||||
export interface ChatMessageProps {
|
||||
message: Message;
|
||||
showDocuments?: boolean;
|
||||
|
|
@ -29,8 +31,7 @@ export interface ChatMessageProps {
|
|||
/**
|
||||
* Renders a single message in chat style (bubble UI)
|
||||
*/
|
||||
export const ChatMessage: React.FC<ChatMessageProps> = ({
|
||||
message,
|
||||
export const ChatMessage: React.FC<ChatMessageProps> = ({ message,
|
||||
showDocuments = true,
|
||||
renderDocument,
|
||||
onFileDelete,
|
||||
|
|
@ -45,6 +46,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||
onMessageDelete,
|
||||
deletingMessages
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const isUser = message.role?.toLowerCase() === 'user';
|
||||
const isError = message.actionProgress === 'fail' || message.actionProgress === 'error';
|
||||
const messageClass = isUser ? styles.messageUser : styles.messageAssistant;
|
||||
|
|
@ -79,7 +81,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||
{isError && (
|
||||
<div className={styles.errorIndicator}>
|
||||
<FaExclamationTriangle className={styles.errorIcon} />
|
||||
<span>Aktion fehlgeschlagen</span>
|
||||
<span>{t('chatMessage.aktionFehlgeschlagen')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -199,7 +201,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||
<button
|
||||
onClick={handleConfirmDelete}
|
||||
className={styles.messageDeleteConfirmBtn}
|
||||
title="Löschen bestätigen"
|
||||
title={t('chatMessage.loeschenBestaetigen')}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<IoIosCheckmark />
|
||||
|
|
@ -207,7 +209,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||
<button
|
||||
onClick={handleCancelDelete}
|
||||
className={styles.messageDeleteCancelBtn}
|
||||
title="Abbrechen"
|
||||
title={t('chatMessage.abbrechen')}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<IoIosClose />
|
||||
|
|
@ -217,7 +219,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|||
<button
|
||||
onClick={handleDeleteClick}
|
||||
className={styles.messageDeleteBtn}
|
||||
title="Nachricht löschen"
|
||||
title={t('chatMessage.nachrichtLoeschen')}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<IoIosTrash />
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import api from '../../../api';
|
|||
import { ContentPreview } from '../../ContentPreview';
|
||||
import styles from './ParcelInfoPanel.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
export interface SelectionSummary {
|
||||
total_area_m2?: number;
|
||||
bauzonen?: Array<{ bauzone: string; parcels: any[]; area_m2: number }>;
|
||||
|
|
@ -114,14 +116,14 @@ export interface BZOInformationResponse {
|
|||
warnings: string[];
|
||||
}
|
||||
|
||||
const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
||||
isOpen,
|
||||
const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({ isOpen,
|
||||
onClose,
|
||||
parcels,
|
||||
selectionSummary,
|
||||
onRemoveParcel,
|
||||
instanceId
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [parcelDocs, setParcelDocs] = useState<Record<string, ParcelDocument[]>>({});
|
||||
const [docsLoading, setDocsLoading] = useState<Record<string, boolean>>({});
|
||||
const [docsError, setDocsError] = useState<Record<string, string>>({});
|
||||
|
|
@ -225,7 +227,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
<div className={styles.content}>
|
||||
{selectionSummary?.total_area_m2 != null && (
|
||||
<div className={styles.aggregatedSection}>
|
||||
<h3 className={styles.aggregatedTitle}>Gesamtfläche</h3>
|
||||
<h3 className={styles.aggregatedTitle}>{t('parcelInfoPanel.gesamtflaeche')}</h3>
|
||||
<p className={styles.aggregatedValue}>
|
||||
{selectionSummary.total_area_m2.toFixed(2)} m²
|
||||
{selectionSummary.total_area_m2 >= 10000 && (
|
||||
|
|
@ -264,7 +266,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
<button
|
||||
className={styles.removeButton}
|
||||
onClick={() => onRemoveParcel(parcelData.parcel.id)}
|
||||
title="Parzelle entfernen"
|
||||
title={t('parcelInfoPanel.parzelleEntfernen')}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
|
|
@ -315,7 +317,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
)}
|
||||
{parcelData.parcel.area_m2 !== undefined && (
|
||||
<div className={styles.infoItem}>
|
||||
<span className={styles.label}>Fläche:</span>
|
||||
<span className={styles.label}>{t('parcelInfoPanel.flaeche')}</span>
|
||||
<span className={styles.value}>
|
||||
{parcelData.parcel.area_m2.toFixed(2)} m²
|
||||
{parcelData.parcel.area_m2 >= 10000 && (
|
||||
|
|
@ -328,7 +330,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
)}
|
||||
{parcelData.parcel.realestate_type && (
|
||||
<div className={styles.infoItem}>
|
||||
<span className={styles.label}>Grundstückstyp:</span>
|
||||
<span className={styles.label}>{t('parcelInfoPanel.grundstueckstyp')}</span>
|
||||
<span className={styles.value}>{parcelData.parcel.realestate_type}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -342,7 +344,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
<div className={styles.bzoSection}>
|
||||
<h4 className={styles.subSectionTitle}>Bauzonenverordnung</h4>
|
||||
{docsLoading[parcelData.parcel.id] && (
|
||||
<p className={styles.bzoHint}>Dokumente werden geladen...</p>
|
||||
<p className={styles.bzoHint}>{t('parcelInfoPanel.dokumenteWerdenGeladen')}</p>
|
||||
)}
|
||||
{docsError[parcelData.parcel.id] && (
|
||||
<p className={styles.bzoError}>{docsError[parcelData.parcel.id]}</p>
|
||||
|
|
@ -360,7 +362,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
fileName: doc.fileName,
|
||||
mimeType: doc.mimeType
|
||||
})}
|
||||
title="Dokument öffnen"
|
||||
title={t('parcelInfoPanel.dokumentOeffnen')}
|
||||
>
|
||||
<FaEye /> Öffnen
|
||||
</button>
|
||||
|
|
@ -378,7 +380,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
areaForMach
|
||||
)}
|
||||
disabled={extractLoading[parcelData.parcel.id]}
|
||||
title="Inhalt mit LangGraph extrahieren (inkl. Machbarkeitsstudie)"
|
||||
title={t('parcelInfoPanel.inhaltMitLanggraphExtrahierenInkl')}
|
||||
>
|
||||
{extractLoading[parcelData.parcel.id] ? (
|
||||
<FaSync className={styles.spin} />
|
||||
|
|
@ -402,7 +404,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
<>
|
||||
{fakten.length > 0 && (
|
||||
<div className={styles.bzoFakten}>
|
||||
<span className={styles.label}>Fakten aus BZO</span>
|
||||
<span className={styles.label}>{t('parcelInfoPanel.faktenAusBzo')}</span>
|
||||
<ul className={styles.rulesList}>
|
||||
{fakten.map((row: { item: string; value: string; source?: string }, i: number) => (
|
||||
<li key={i}>
|
||||
|
|
@ -419,7 +421,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
)}
|
||||
{vorschlaege.length > 0 && (
|
||||
<div className={styles.bzoSuggestions}>
|
||||
<span className={styles.label}>Vorschläge</span>
|
||||
<span className={styles.label}>{t('parcelInfoPanel.vorschlaege')}</span>
|
||||
<ul className={styles.rulesList}>
|
||||
{vorschlaege.map((row: { item: string; value: string; is_section?: boolean } | string, i: number) => {
|
||||
if (typeof row === 'string') return <li key={i}>{row}</li>;
|
||||
|
|
@ -438,7 +440,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
)}
|
||||
{zusatzinfo.length > 0 && (
|
||||
<div className={styles.bzoZusatzinfo}>
|
||||
<span className={styles.label}>Weiterführende Bestimmungen</span>
|
||||
<span className={styles.label}>{t('parcelInfoPanel.weiterfuehrendeBestimmungen')}</span>
|
||||
<div className={styles.zusatzinfoList}>
|
||||
{zusatzinfo.map((art: { article_label: string; article_title: string; text: string; source?: string }, i: number) => (
|
||||
<details key={i} className={styles.zusatzinfoItem}>
|
||||
|
|
@ -489,7 +491,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
})()}
|
||||
{import.meta.env.DEV && (
|
||||
<details className={styles.zoneDetails}>
|
||||
<summary className={styles.zoneSummary}>Details anzeigen</summary>
|
||||
<summary className={styles.zoneSummary}>{t('parcelInfoPanel.detailsAnzeigen')}</summary>
|
||||
<pre className={styles.zoneData}>
|
||||
{JSON.stringify(parcelData.parcel.zone, null, 2)}
|
||||
</pre>
|
||||
|
|
@ -530,7 +532,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
<button
|
||||
className={styles.removeButton}
|
||||
onClick={() => onRemoveParcel(parcelData.parcel.id)}
|
||||
title="Parzelle entfernen"
|
||||
title={t('parcelInfoPanel.parzelleEntfernen')}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
|
|
@ -581,7 +583,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
)}
|
||||
{parcelData.parcel.area_m2 !== undefined && (
|
||||
<div className={styles.infoItem}>
|
||||
<span className={styles.label}>Fläche:</span>
|
||||
<span className={styles.label}>{t('parcelInfoPanel.flaeche')}</span>
|
||||
<span className={styles.value}>
|
||||
{parcelData.parcel.area_m2.toFixed(2)} m²
|
||||
{parcelData.parcel.area_m2 >= 10000 && (
|
||||
|
|
@ -594,7 +596,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
)}
|
||||
{parcelData.parcel.realestate_type && (
|
||||
<div className={styles.infoItem}>
|
||||
<span className={styles.label}>Grundstückstyp:</span>
|
||||
<span className={styles.label}>{t('parcelInfoPanel.grundstueckstyp')}</span>
|
||||
<span className={styles.value}>{parcelData.parcel.realestate_type}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -608,7 +610,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
<div className={styles.bzoSection}>
|
||||
<h4 className={styles.subSectionTitle}>Bauzonenverordnung</h4>
|
||||
{docsLoading[parcelData.parcel.id] && (
|
||||
<p className={styles.bzoHint}>Dokumente werden geladen...</p>
|
||||
<p className={styles.bzoHint}>{t('parcelInfoPanel.dokumenteWerdenGeladen')}</p>
|
||||
)}
|
||||
{docsError[parcelData.parcel.id] && (
|
||||
<p className={styles.bzoError}>{docsError[parcelData.parcel.id]}</p>
|
||||
|
|
@ -626,7 +628,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
fileName: doc.fileName,
|
||||
mimeType: doc.mimeType
|
||||
})}
|
||||
title="Dokument öffnen"
|
||||
title={t('parcelInfoPanel.dokumentOeffnen')}
|
||||
>
|
||||
<FaEye /> Öffnen
|
||||
</button>
|
||||
|
|
@ -644,7 +646,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
areaForMachFlat
|
||||
)}
|
||||
disabled={extractLoading[parcelData.parcel.id]}
|
||||
title="Inhalt mit LangGraph extrahieren (inkl. Machbarkeitsstudie)"
|
||||
title={t('parcelInfoPanel.inhaltMitLanggraphExtrahierenInkl')}
|
||||
>
|
||||
{extractLoading[parcelData.parcel.id] ? (
|
||||
<FaSync className={styles.spin} />
|
||||
|
|
@ -668,7 +670,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
<>
|
||||
{fakten.length > 0 && (
|
||||
<div className={styles.bzoFakten}>
|
||||
<span className={styles.label}>Fakten aus BZO</span>
|
||||
<span className={styles.label}>{t('parcelInfoPanel.faktenAusBzo')}</span>
|
||||
<ul className={styles.rulesList}>
|
||||
{fakten.map((row: { item: string; value: string; source?: string }, i: number) => (
|
||||
<li key={i}>
|
||||
|
|
@ -685,7 +687,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
)}
|
||||
{vorschlaege.length > 0 && (
|
||||
<div className={styles.bzoSuggestions}>
|
||||
<span className={styles.label}>Vorschläge</span>
|
||||
<span className={styles.label}>{t('parcelInfoPanel.vorschlaege')}</span>
|
||||
<ul className={styles.rulesList}>
|
||||
{vorschlaege.map((row: { item: string; value: string; is_section?: boolean } | string, i: number) => {
|
||||
if (typeof row === 'string') return <li key={i}>{row}</li>;
|
||||
|
|
@ -704,7 +706,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
)}
|
||||
{zusatzinfo.length > 0 && (
|
||||
<div className={styles.bzoZusatzinfo}>
|
||||
<span className={styles.label}>Weiterführende Bestimmungen</span>
|
||||
<span className={styles.label}>{t('parcelInfoPanel.weiterfuehrendeBestimmungen')}</span>
|
||||
<div className={styles.zusatzinfoList}>
|
||||
{zusatzinfo.map((art: { article_label: string; article_title: string; text: string; source?: string }, i: number) => (
|
||||
<details key={i} className={styles.zusatzinfoItem}>
|
||||
|
|
@ -753,7 +755,7 @@ const ParcelInfoPanel: React.FC<ParcelInfoPanelProps> = ({
|
|||
})()}
|
||||
{import.meta.env.DEV && (
|
||||
<details className={styles.zoneDetails}>
|
||||
<summary className={styles.zoneSummary}>Details anzeigen</summary>
|
||||
<summary className={styles.zoneSummary}>{t('parcelInfoPanel.detailsAnzeigen')}</summary>
|
||||
<pre className={styles.zoneData}>
|
||||
{JSON.stringify(parcelData.parcel.zone, null, 2)}
|
||||
</pre>
|
||||
|
|
@ -835,12 +837,13 @@ const bzoMarkdownComponents: Record<string, React.ComponentType<MdComponentProps
|
|||
};
|
||||
|
||||
export const BZOInformationDisplay: React.FC<BZOInformationDisplayProps> = ({ data }) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className={styles.bzoContent}>
|
||||
{/* Summary Section */}
|
||||
{data.ai_summary && (
|
||||
<div className={styles.bzoSubSection}>
|
||||
<h5 className={styles.bzoSubTitle}>Zusammenfassung</h5>
|
||||
<h5 className={styles.bzoSubTitle}>{t('parcelInfoPanel.zusammenfassung')}</h5>
|
||||
<div className={styles.bzoSummary}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
|
|
@ -1079,7 +1082,7 @@ export const BZOInformationDisplay: React.FC<BZOInformationDisplayProps> = ({ da
|
|||
{/* Statistics */}
|
||||
{data.extracted_content && (
|
||||
<div className={styles.bzoSubSection}>
|
||||
<h5 className={styles.bzoSubTitle}>Statistiken</h5>
|
||||
<h5 className={styles.bzoSubTitle}>{t('parcelInfoPanel.statistiken')}</h5>
|
||||
<div className={styles.bzoStats}>
|
||||
<div className={styles.bzoStatItem}>
|
||||
<span className={styles.bzoStatLabel}>Zonen:</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import React from 'react';
|
||||
import styles from './Popup.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
// Action button interface
|
||||
export interface PopupAction {
|
||||
label: string;
|
||||
|
|
@ -36,6 +38,7 @@ export function Popup({
|
|||
closable = true,
|
||||
actions = []
|
||||
}: PopupProps) {
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Handle escape key
|
||||
React.useEffect(() => {
|
||||
|
|
@ -105,7 +108,7 @@ export function Popup({
|
|||
<button
|
||||
className={styles.closeButton}
|
||||
onClick={onClose}
|
||||
aria-label="Close"
|
||||
aria-label={t('popup.close')}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|||
import { FaCheckCircle, FaExclamationCircle, FaExclamationTriangle, FaInfoCircle, FaTimes } from 'react-icons/fa';
|
||||
import styles from './Toast.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
export interface ToastData {
|
||||
|
|
@ -31,6 +33,7 @@ const _getIcon = (type: ToastType) => {
|
|||
};
|
||||
|
||||
export const Toast: React.FC<ToastProps> = ({ toast, onClose }) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<motion.div
|
||||
className={`${styles.toast} ${styles[toast.type]}`}
|
||||
|
|
@ -50,7 +53,7 @@ export const Toast: React.FC<ToastProps> = ({ toast, onClose }) => {
|
|||
<button
|
||||
className={styles.closeButton}
|
||||
onClick={() => onClose(toast.id)}
|
||||
aria-label="Schließen"
|
||||
aria-label={t('toast.schliessen')}
|
||||
>
|
||||
<FaTimes />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import {
|
|||
} from '../../../utils/attributeTypeMapper';
|
||||
import type { AttributeType } from '../../../utils/attributeTypeMapper';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
// Field configuration interface for ViewForm
|
||||
export interface ViewFieldConfig {
|
||||
key: string;
|
||||
|
|
@ -32,6 +34,8 @@ export function ViewForm<T extends Record<string, any>>({
|
|||
|
||||
// Format value based on field type
|
||||
const formatValue = (field: ViewFieldConfig, value: any): string => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Use custom formatter if provided
|
||||
if (field.formatter) {
|
||||
return field.formatter(value);
|
||||
|
|
@ -46,7 +50,7 @@ export function ViewForm<T extends Record<string, any>>({
|
|||
if (field.type) {
|
||||
// Boolean/Checkbox types
|
||||
if (isCheckboxType(field.type)) {
|
||||
return value ? 'Yes' : 'No';
|
||||
return value ? t('viewForm.yes') : t('viewForm.no');
|
||||
}
|
||||
|
||||
// Select/Enum types
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import React, { useMemo } from 'react';
|
|||
import { WorkflowStatusProps, WorkflowStatusType } from './WorkflowStatusTypes';
|
||||
import styles from './WorkflowStatus.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
const _STATUS_MAP: Record<string, WorkflowStatusType> = {
|
||||
success: 'completed',
|
||||
completed: 'completed',
|
||||
|
|
@ -43,6 +45,7 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
|||
isRunning,
|
||||
latestStats
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
// Use workflow status and round from API response, fallback to extracting from logs
|
||||
const workflowStatus = useMemo(() => {
|
||||
if (workflowStatusFromApi) {
|
||||
|
|
@ -66,7 +69,7 @@ const WorkflowStatus: React.FC<WorkflowStatusProps> = ({
|
|||
{/* Status and Round Badges */}
|
||||
<div className={styles.workflowStatus}>
|
||||
{showSpinner && (
|
||||
<div className={styles.spinner} aria-label="Workflow running" />
|
||||
<div className={styles.spinner} aria-label={t('workflowStatus.workflowRunning')} />
|
||||
)}
|
||||
{workflowStatus.status && (
|
||||
<span className={styles.statusBadge} data-status={workflowStatus.status}>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import type { UdbContext } from './UnifiedDataBar';
|
|||
import api from '../../api';
|
||||
import styles from './ChatsTab.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
interface ChatItem {
|
||||
id: string;
|
||||
label: string;
|
||||
|
|
@ -50,8 +52,7 @@ function _formatRelativeTime(dateStr?: string | number): string {
|
|||
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
const ChatsTab: React.FC<ChatsTabProps> = ({
|
||||
context,
|
||||
const ChatsTab: React.FC<ChatsTabProps> = ({ context,
|
||||
onSelectChat,
|
||||
onDragStart,
|
||||
activeWorkflowId,
|
||||
|
|
@ -59,6 +60,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({
|
|||
onRenameChat,
|
||||
onDeleteChat,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [groups, setGroups] = useState<ChatGroup[]>([]);
|
||||
const [flatMode, setFlatMode] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
|
|
@ -307,7 +309,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({
|
|||
<button
|
||||
className={`${styles.actionBtn} ${styles.actionBtnDanger}`}
|
||||
onClick={async (e) => { e.stopPropagation(); await onDeleteChat(chat.id); _loadChats(); }}
|
||||
title="Löschen"
|
||||
title={t('chatsTab.loeschen')}
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
|
|
@ -329,7 +331,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({
|
|||
return labels[code] || code;
|
||||
};
|
||||
|
||||
if (loading) return <div className={styles.loading}>Lade Chats...</div>;
|
||||
if (loading) return <div className={styles.loading}>{t('chatsTab.ladeChats')}</div>;
|
||||
|
||||
return (
|
||||
<div className={styles.chatsTab}>
|
||||
|
|
@ -337,12 +339,12 @@ const ChatsTab: React.FC<ChatsTabProps> = ({
|
|||
<input
|
||||
className={styles.search}
|
||||
type="text"
|
||||
placeholder="Suchen..."
|
||||
placeholder={t('chatsTab.suchen')}
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
{onCreateNew && (
|
||||
<button className={styles.createBtn} onClick={() => { onCreateNew(); setTimeout(_loadChats, 500); }} title="Neuer Chat">
|
||||
<button className={styles.createBtn} onClick={() => { onCreateNew(); setTimeout(_loadChats, 500); }} title={t('chatsTab.neuerChat')}>
|
||||
+
|
||||
</button>
|
||||
)}
|
||||
|
|
@ -434,7 +436,7 @@ const ChatsTab: React.FC<ChatsTabProps> = ({
|
|||
|
||||
{_allChats.length === 0 && (
|
||||
<div className={styles.emptyState}>
|
||||
{filter === 'archived' ? 'Keine archivierten Chats.' : 'Keine aktiven Chats.'}
|
||||
{filter === 'archived' ? t('chatsTab.keineArchiviertenChats') : t('chatsTab.keineAktivenChats')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import type { FileNode } from '../../components/FolderTree/FolderTree';
|
|||
import { useFileContext } from '../../contexts/FileContext';
|
||||
import styles from './FilesTab.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
interface FileEntry {
|
||||
id: string;
|
||||
fileName: string;
|
||||
|
|
@ -23,6 +25,7 @@ interface FilesTabProps {
|
|||
}
|
||||
|
||||
const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
||||
const { t } = useLanguage();
|
||||
const [files, setFiles] = useState<FileEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
|
@ -230,7 +233,7 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
|||
}
|
||||
}, [_loadFiles]);
|
||||
|
||||
if (loading) return <div className={styles.loading}>Lade Dateien...</div>;
|
||||
if (loading) return <div className={styles.loading}>{t('filesTab.ladeDateien')}</div>;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -258,7 +261,7 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
|||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, color: '#F25843' }}
|
||||
title="Upload files"
|
||||
title={t('filesTab.uploadFiles')}
|
||||
>
|
||||
{uploading ? '...' : '+'}
|
||||
</button>
|
||||
|
|
@ -281,7 +284,7 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
|||
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Dateien suchen..."
|
||||
placeholder={t('filesTab.dateienSuchen')}
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
style={{
|
||||
|
|
@ -322,7 +325,7 @@ const FilesTab: React.FC<FilesTabProps> = ({ context, onFileSelect }) => {
|
|||
|
||||
{_fileNodes.length === 0 && (
|
||||
<div style={{ fontSize: 12, color: '#999', textAlign: 'center', padding: 16 }}>
|
||||
{searchQuery ? 'Keine Dateien gefunden' : 'Keine Dateien. Drag & Drop zum Hochladen.'}
|
||||
{searchQuery ? t('filesTab.keineDateienGefunden') : t('filesTab.keineDateienDragDropZum')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import api from '../../api';
|
|||
import { getPageIcon } from '../../config/pageRegistry';
|
||||
import styles from './SourcesTab.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
/* ─── Types (inline, no external imports) ────────────────────────────── */
|
||||
|
||||
interface UdbDataSource {
|
||||
|
|
@ -345,6 +347,7 @@ function _Spinner(): React.ReactElement {
|
|||
/* ─── Component ──────────────────────────────────────────────────────── */
|
||||
|
||||
const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) => {
|
||||
const { t } = useLanguage();
|
||||
const instanceId = context.instanceId;
|
||||
|
||||
/* ── Active sources (fetched internally) ── */
|
||||
|
|
@ -850,14 +853,14 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
fontSize: 13, padding: '0 2px', lineHeight: 1,
|
||||
opacity: ds.neutralize ? 1 : 0.35,
|
||||
}}
|
||||
title={ds.neutralize ? 'Neutralize: ON (click to deactivate)' : 'Neutralize: OFF (click to activate)'}
|
||||
title={ds.neutralize ? t('sourcesTab.neutralizeOnClickToDeactivate') : t('sourcesTab.neutralizeOffClickToActivate')}
|
||||
>
|
||||
{'\uD83D\uDD12'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => _removeDatasource(ds.id)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }}
|
||||
title="Entfernen"
|
||||
title={t('sourcesTab.entfernen')}
|
||||
>
|
||||
{'\u2715'}
|
||||
</button>
|
||||
|
|
@ -953,7 +956,7 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
<button
|
||||
onClick={() => { group.items.forEach(fds => _removeFeatureDataSource(fds.id)); }}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px' }}
|
||||
title="Remove all tables for this record"
|
||||
title={t('sourcesTab.removeAllTablesForThis')}
|
||||
>
|
||||
{'\u2715'}
|
||||
</button>
|
||||
|
|
@ -983,14 +986,14 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
<button
|
||||
onClick={() => _toggleFeatureNeutralize(fds)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 12, padding: '0 2px', lineHeight: 1, opacity: fds.neutralize ? 1 : 0.35 }}
|
||||
title={fds.neutralize ? 'Neutralize: ON' : 'Neutralize: OFF'}
|
||||
title={fds.neutralize ? t('sourcesTab.neutralizeOn') : t('sourcesTab.neutralizeOff')}
|
||||
>
|
||||
{'\uD83D\uDD12'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => _removeFeatureDataSource(fds.id)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 10, color: '#999', padding: '0 2px' }}
|
||||
title="Remove"
|
||||
title={t('sourcesTab.remove')}
|
||||
>
|
||||
{'\u2715'}
|
||||
</button>
|
||||
|
|
@ -1026,14 +1029,14 @@ const SourcesTab: React.FC<SourcesTabProps> = ({ context, onSourcesChanged }) =>
|
|||
<button
|
||||
onClick={() => _toggleFeatureNeutralize(fds)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 13, padding: '0 2px', lineHeight: 1, opacity: fds.neutralize ? 1 : 0.35 }}
|
||||
title={fds.neutralize ? 'Neutralize: ON' : 'Neutralize: OFF'}
|
||||
title={fds.neutralize ? t('sourcesTab.neutralizeOn') : t('sourcesTab.neutralizeOff')}
|
||||
>
|
||||
{'\uD83D\uDD12'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => _removeFeatureDataSource(fds.id)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: 11, color: '#999', padding: '0 2px' }}
|
||||
title="Entfernen"
|
||||
title={t('sourcesTab.entfernen')}
|
||||
>
|
||||
{'\u2715'}
|
||||
</button>
|
||||
|
|
@ -1110,6 +1113,7 @@ interface _TreeNodeViewProps {
|
|||
const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
|
||||
node, depth, onToggle, onAdd, isAdded, addingPath,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const hasChildren = node.type !== 'file';
|
||||
const chevron = hasChildren
|
||||
|
|
@ -1163,13 +1167,13 @@ const _TreeNodeView: React.FC<_TreeNodeViewProps> = ({
|
|||
opacity: isAdding ? 0.5 : 1,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
title="Add as data source"
|
||||
title={t('sourcesTab.addAsDataSource')}
|
||||
>
|
||||
{isAdding ? '...' : '+ Add'}
|
||||
</button>
|
||||
)}
|
||||
{canAdd && alreadyAdded && (
|
||||
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title="Already added">
|
||||
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title={t('sourcesTab.alreadyAdded')}>
|
||||
{'\u2713'}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -1396,6 +1400,7 @@ interface _FeatureTableRowProps {
|
|||
const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
|
||||
featureNode, table, onAdd, isAdded, isAdding,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const tableLabel = table.label?.en || table.label?.de || table.tableName;
|
||||
|
||||
|
|
@ -1426,13 +1431,13 @@ const _FeatureTableRow: React.FC<_FeatureTableRowProps> = ({
|
|||
fontSize: 10, color: '#7b1fa2', padding: '1px 5px',
|
||||
opacity: isAdding ? 0.5 : 1, flexShrink: 0,
|
||||
}}
|
||||
title="Add as feature data source"
|
||||
title={t('sourcesTab.addAsFeatureDataSource')}
|
||||
>
|
||||
{isAdding ? '...' : '+ Add'}
|
||||
</button>
|
||||
)}
|
||||
{isAdded && (
|
||||
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title="Already added">
|
||||
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title={t('sourcesTab.alreadyAdded')}>
|
||||
{'\u2713'}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -1537,6 +1542,7 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({
|
|||
featureNode: _featureNode, record, childTables, allTables: _allTables,
|
||||
onToggle, onAdd, isAdded, isAdding,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const chevron = record.expanded ? '\u25BE' : '\u25B8';
|
||||
|
||||
|
|
@ -1572,13 +1578,13 @@ const _ParentRecordRow: React.FC<_ParentRecordRowProps> = ({
|
|||
fontSize: 10, color: '#7b1fa2', padding: '1px 5px',
|
||||
opacity: isAdding ? 0.5 : 1, flexShrink: 0,
|
||||
}}
|
||||
title="Add all tables for this record"
|
||||
title={t('sourcesTab.addAllTablesForThis')}
|
||||
>
|
||||
{isAdding ? '...' : '+ Add'}
|
||||
</button>
|
||||
)}
|
||||
{isAdded && (
|
||||
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title="Already added">
|
||||
<span style={{ fontSize: 10, color: '#4caf50', flexShrink: 0 }} title={t('sourcesTab.alreadyAdded')}>
|
||||
{'\u2713'}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,8 @@ export const SidebarProvider: React.FC<SidebarProviderProps> = ({ children }) =>
|
|||
|
||||
// Helper function to resolve node name
|
||||
const resolveNodeName = (pathSegment: string, fullPath: string, page?: GenericPageData): string => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
if (page) {
|
||||
return resolveLanguageText(page.name, t);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,23 +19,25 @@ export type AccessLevel = 'n' | 'm' | 'g' | 'a' | null;
|
|||
// ACCESS LEVEL LABELS
|
||||
// =============================================================================
|
||||
|
||||
export const ACCESS_LEVEL_OPTIONS: { value: 'n' | 'm' | 'g' | 'a'; label: string; color: string }[] = [
|
||||
{ value: 'n', label: 'Keine', color: '#e53e3e' },
|
||||
{ value: 'm', label: 'Eigene', color: '#d69e2e' },
|
||||
{ value: 'g', label: 'Gruppe', color: '#3182ce' },
|
||||
{ value: 'a', label: 'Alle', color: '#38a169' },
|
||||
export function _getAccessLevelOptions(t: (key: string) => string): { value: 'n' | 'm' | 'g' | 'a'; label: string; color: string }[] {
|
||||
return [
|
||||
{ value: 'n', label: t('useAccessRules.keine'), color: '#e53e3e' },
|
||||
{ value: 'm', label: t('useAccessRules.eigene'), color: '#d69e2e' },
|
||||
{ value: 'g', label: t('useAccessRules.gruppe'), color: '#3182ce' },
|
||||
{ value: 'a', label: t('useAccessRules.alle'), color: '#38a169' },
|
||||
];
|
||||
}
|
||||
|
||||
export const getAccessLevelLabel = (level: AccessLevel | null): string => {
|
||||
if (!level) return '-';
|
||||
const option = ACCESS_LEVEL_OPTIONS.find(opt => opt.value === level);
|
||||
return option?.label || level;
|
||||
const _accessLevelColors: Record<string, string> = {
|
||||
n: '#e53e3e',
|
||||
m: '#d69e2e',
|
||||
g: '#3182ce',
|
||||
a: '#38a169',
|
||||
};
|
||||
|
||||
export const getAccessLevelColor = (level: AccessLevel | null): string => {
|
||||
if (!level) return '#718096';
|
||||
const option = ACCESS_LEVEL_OPTIONS.find(opt => opt.value === level);
|
||||
return option?.color || '#718096';
|
||||
return _accessLevelColors[level] || '#718096';
|
||||
};
|
||||
|
||||
export interface AccessRule {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
* // Render <ConfirmDialog /> once in the component tree.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import React, { useState, useCallback, useRef, useMemo } from 'react';
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
export interface ConfirmOptions {
|
||||
title?: string;
|
||||
|
|
@ -22,17 +23,18 @@ interface ConfirmState {
|
|||
resolve: (value: boolean) => void;
|
||||
}
|
||||
|
||||
const _defaults: Required<ConfirmOptions> = {
|
||||
title: 'Bestätigung',
|
||||
confirmLabel: 'Bestätigen',
|
||||
cancelLabel: 'Abbrechen',
|
||||
variant: 'primary',
|
||||
};
|
||||
|
||||
export function useConfirm() {
|
||||
const { t } = useLanguage();
|
||||
const [state, setState] = useState<ConfirmState | null>(null);
|
||||
const resolveRef = useRef<((v: boolean) => void) | null>(null);
|
||||
|
||||
const _defaults: Required<ConfirmOptions> = useMemo(() => ({
|
||||
title: t('confirm.title'),
|
||||
confirmLabel: t('confirm.confirmLabel'),
|
||||
cancelLabel: t('confirm.cancelLabel'),
|
||||
variant: 'primary' as const,
|
||||
}), [t]);
|
||||
|
||||
const confirm = useCallback((message: string, options?: ConfirmOptions): Promise<boolean> => {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
resolveRef.current = resolve;
|
||||
|
|
@ -42,7 +44,7 @@ export function useConfirm() {
|
|||
resolve,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
}, [_defaults]);
|
||||
|
||||
const _handleConfirm = useCallback(() => {
|
||||
resolveRef.current?.(true);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,3 @@
|
|||
export type {
|
||||
Workflow,
|
||||
WorkflowMessage,
|
||||
WorkflowLog,
|
||||
StartWorkflowRequest,
|
||||
StartWorkflowResponse,
|
||||
ChatDataResponse
|
||||
} from '../api/workflowApi';
|
||||
|
||||
export interface UserInputRequest {
|
||||
input: string;
|
||||
files?: any[];
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
* // Render <PromptDialog /> once in the component tree.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import React, { useState, useCallback, useRef, useMemo } from 'react';
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
export interface PromptOptions {
|
||||
title?: string;
|
||||
|
|
@ -25,20 +26,21 @@ interface PromptState {
|
|||
resolve: (value: string | null) => void;
|
||||
}
|
||||
|
||||
const _defaults: Required<PromptOptions> = {
|
||||
title: 'Eingabe',
|
||||
confirmLabel: 'OK',
|
||||
cancelLabel: 'Abbrechen',
|
||||
placeholder: '',
|
||||
defaultValue: '',
|
||||
variant: 'primary',
|
||||
};
|
||||
|
||||
export function usePrompt() {
|
||||
const { t } = useLanguage();
|
||||
const [state, setState] = useState<PromptState | null>(null);
|
||||
const resolveRef = useRef<((v: string | null) => void) | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const _defaults: Required<PromptOptions> = useMemo(() => ({
|
||||
title: t('prompt.title'),
|
||||
confirmLabel: t('prompt.confirmLabel'),
|
||||
cancelLabel: t('prompt.cancelLabel'),
|
||||
placeholder: '',
|
||||
defaultValue: '',
|
||||
variant: 'primary' as const,
|
||||
}), [t]);
|
||||
|
||||
const prompt = useCallback((message: string, options?: PromptOptions): Promise<string | null> => {
|
||||
return new Promise<string | null>((resolve) => {
|
||||
resolveRef.current = resolve;
|
||||
|
|
@ -48,7 +50,7 @@ export function usePrompt() {
|
|||
resolve,
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
}, [_defaults]);
|
||||
|
||||
const _handleConfirm = useCallback(() => {
|
||||
const val = inputRef.current?.value ?? '';
|
||||
|
|
|
|||
|
|
@ -1,23 +1,82 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useApiRequest } from './useApi';
|
||||
import { fetchAttributes as fetchAttributesApi } from '../api/attributesApi';
|
||||
import type { AttributeDefinition, ApiRequestFunction } from '../api/attributesApi';
|
||||
import {
|
||||
deleteWorkflowApi,
|
||||
deleteWorkflowsApi,
|
||||
updateWorkflowApi,
|
||||
fetchWorkflows as fetchWorkflowsApi,
|
||||
fetchWorkflow as fetchWorkflowByIdApi,
|
||||
fetchAttributes as fetchAttributesApi,
|
||||
startWorkflowApi,
|
||||
stopWorkflowApi,
|
||||
deleteMessageApi,
|
||||
deleteFileFromMessageApi,
|
||||
type Workflow,
|
||||
type AttributeDefinition,
|
||||
type StartWorkflowRequest
|
||||
fetchWorkflows as fetchWorkflowsFromApi,
|
||||
fetchWorkflow as fetchWorkflowFromApi,
|
||||
deleteWorkflow as deleteWorkflowFromApi,
|
||||
updateWorkflow as updateWorkflowFromApi,
|
||||
} from '../api/workflowApi';
|
||||
import { useWorkflowSelection } from '../contexts/WorkflowSelectionContext';
|
||||
import { usePermissions, type UserPermissions } from './usePermissions';
|
||||
|
||||
export type StartWorkflowRequest = Record<string, unknown>;
|
||||
|
||||
function _workflowsInstanceIdFromBaseUrl(apiBaseUrl: string | undefined): string | null {
|
||||
if (!apiBaseUrl) return null;
|
||||
const m = apiBaseUrl.match(/^\/api\/workflows\/([^/]+)$/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
async function _deleteWorkflowsSequential(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
workflowIds: string[],
|
||||
) {
|
||||
for (const id of workflowIds) {
|
||||
await deleteWorkflowFromApi(request, instanceId, id);
|
||||
}
|
||||
}
|
||||
|
||||
async function startWorkflowApi(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
workflowData: StartWorkflowRequest,
|
||||
options?: { workflowId?: string; workflowMode?: 'Dynamic' | 'Automation' },
|
||||
) {
|
||||
return await request({
|
||||
url: `/api/workflows/${instanceId}/execute`,
|
||||
method: 'post',
|
||||
data: {
|
||||
workflowId: options?.workflowId ?? (workflowData as { workflowId?: string }).workflowId,
|
||||
payload: workflowData,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function stopWorkflowApi(request: ApiRequestFunction, instanceId: string, workflowId: string) {
|
||||
await request({
|
||||
url: `/api/workspace/${instanceId}/${workflowId}/stop`,
|
||||
method: 'post',
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteMessageApi(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
workflowId: string,
|
||||
messageId: string,
|
||||
) {
|
||||
await request({
|
||||
url: `/api/workspace/${instanceId}/workflows/${workflowId}/messages/${messageId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteFileFromMessageApi(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
workflowId: string,
|
||||
messageId: string,
|
||||
fileId: string,
|
||||
) {
|
||||
await request({
|
||||
url: `/api/workspace/${instanceId}/workflows/${workflowId}/messages/${messageId}/files/${fileId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
// Workflow interface matching backend
|
||||
export interface UserWorkflow {
|
||||
id: string;
|
||||
|
|
@ -28,8 +87,8 @@ export interface UserWorkflow {
|
|||
[key: string]: any; // Allow additional properties
|
||||
}
|
||||
|
||||
// Re-export AttributeDefinition from workflowApi
|
||||
export type { AttributeDefinition } from '../api/workflowApi';
|
||||
// Re-export AttributeDefinition from attributesApi
|
||||
export type { AttributeDefinition } from '../api/attributesApi';
|
||||
|
||||
// Attribute option interface (from backend)
|
||||
export interface AttributeOption {
|
||||
|
|
@ -65,7 +124,7 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
|
|||
totalItems: number;
|
||||
totalPages: number;
|
||||
} | null>(null);
|
||||
const { request, isLoading: loading, error } = useApiRequest<null, Workflow[]>();
|
||||
const { request, isLoading: loading, error } = useApiRequest<null, unknown>();
|
||||
const { checkPermission } = usePermissions();
|
||||
|
||||
// Fetch attributes from backend
|
||||
|
|
@ -103,38 +162,33 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
|
|||
|
||||
const fetchWorkflowsData = useCallback(async (params?: PaginationParams) => {
|
||||
try {
|
||||
const requestParams: any = {};
|
||||
|
||||
// Build pagination object if provided
|
||||
if (!apiBaseUrl) {
|
||||
console.error('useUserWorkflows: apiBaseUrl is required (missing instanceId/featureCode)');
|
||||
return;
|
||||
}
|
||||
const instanceId = _workflowsInstanceIdFromBaseUrl(apiBaseUrl);
|
||||
if (!instanceId) {
|
||||
console.error('useUserWorkflows: could not parse instanceId from apiBaseUrl');
|
||||
return;
|
||||
}
|
||||
let listParams: { pagination?: Record<string, unknown> } | undefined = undefined;
|
||||
if (params) {
|
||||
const paginationObj: any = {};
|
||||
|
||||
const paginationObj: Record<string, unknown> = {};
|
||||
if (params.page !== undefined) paginationObj.page = params.page;
|
||||
if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
|
||||
if (params.sort) paginationObj.sort = params.sort;
|
||||
if (params.filters) paginationObj.filters = params.filters;
|
||||
if (params.search) paginationObj.search = params.search;
|
||||
|
||||
if (Object.keys(paginationObj).length > 0) {
|
||||
requestParams.pagination = JSON.stringify(paginationObj);
|
||||
listParams = { pagination: paginationObj };
|
||||
}
|
||||
}
|
||||
|
||||
let data: any;
|
||||
if (!apiBaseUrl) {
|
||||
console.error('useUserWorkflows: apiBaseUrl is required (missing instanceId/featureCode)');
|
||||
return;
|
||||
}
|
||||
const url = `${apiBaseUrl}/workflows`;
|
||||
if (Object.keys(requestParams).length > 0) {
|
||||
data = await request({ url, method: 'get', params: requestParams });
|
||||
} else {
|
||||
data = await fetchWorkflowsApi(request, undefined, apiBaseUrl);
|
||||
}
|
||||
const data: unknown = await fetchWorkflowsFromApi(request, instanceId, listParams);
|
||||
|
||||
// Handle paginated response
|
||||
if (data && typeof data === 'object' && 'items' in data) {
|
||||
const items = Array.isArray(data.items) ? data.items : [];
|
||||
if (data && typeof data === 'object' && data !== null && 'items' in data) {
|
||||
const d = data as { items?: unknown; pagination?: unknown };
|
||||
const items = Array.isArray(d.items) ? d.items : [];
|
||||
// Map API response to our frontend model
|
||||
const mappedWorkflows = items.map((apiWorkflow: any): UserWorkflow => {
|
||||
return {
|
||||
|
|
@ -147,8 +201,8 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
|
|||
};
|
||||
});
|
||||
setWorkflows(mappedWorkflows);
|
||||
if (data.pagination) {
|
||||
setPagination(data.pagination);
|
||||
if (d.pagination && typeof d.pagination === 'object') {
|
||||
setPagination(d.pagination as any);
|
||||
}
|
||||
} else {
|
||||
// Handle non-paginated response (backward compatibility)
|
||||
|
|
@ -192,8 +246,10 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
|
|||
// Fetch a single workflow by ID
|
||||
const fetchWorkflowById = useCallback(async (workflowId: string): Promise<UserWorkflow | null> => {
|
||||
try {
|
||||
const workflow = await fetchWorkflowByIdApi(request, workflowId, apiBaseUrl);
|
||||
return workflow as UserWorkflow | null;
|
||||
const instanceId = _workflowsInstanceIdFromBaseUrl(apiBaseUrl);
|
||||
if (!instanceId) return null;
|
||||
const workflow = await fetchWorkflowFromApi(request, instanceId, workflowId);
|
||||
return workflow as unknown as UserWorkflow | null;
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching workflow by ID:', error);
|
||||
return null;
|
||||
|
|
@ -379,7 +435,7 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
|
|||
|
||||
// Workflow operations hook - pass instanceId and featureCode when in feature context for feature-scoped API
|
||||
export function useWorkflowOperations(options?: { instanceId?: string; featureCode?: string }) {
|
||||
const apiBaseUrl = getWorkflowApiBaseUrl(options?.instanceId, options?.featureCode);
|
||||
const instanceId = options?.instanceId;
|
||||
const [startingWorkflow, setStartingWorkflow] = useState(false);
|
||||
const [stoppingWorkflows, setStoppingWorkflows] = useState<Set<string>>(new Set());
|
||||
const [deletingWorkflows, setDeletingWorkflows] = useState<Set<string>>(new Set());
|
||||
|
|
@ -479,7 +535,10 @@ export function useWorkflowOperations(options?: { instanceId?: string; featureCo
|
|||
workflowId,
|
||||
setDeletingWorkflows,
|
||||
setDeleteError,
|
||||
() => deleteWorkflowApi(request, workflowId, apiBaseUrl),
|
||||
() => {
|
||||
if (!instanceId) throw new Error('instanceId required');
|
||||
return deleteWorkflowFromApi(request, instanceId, workflowId);
|
||||
},
|
||||
{
|
||||
default: 'Failed to delete workflow',
|
||||
notFound: 'Workflow not found or has already been deleted.',
|
||||
|
|
@ -514,8 +573,8 @@ export function useWorkflowOperations(options?: { instanceId?: string; featureCo
|
|||
});
|
||||
|
||||
try {
|
||||
// Delete workflows one by one since there's no bulk delete endpoint
|
||||
await deleteWorkflowsApi(request, workflowIds, apiBaseUrl);
|
||||
if (!instanceId) throw new Error('instanceId required');
|
||||
await _deleteWorkflowsSequential(request, instanceId, workflowIds);
|
||||
|
||||
// Add a small delay to ensure backend has time to process
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
|
@ -550,7 +609,10 @@ export function useWorkflowOperations(options?: { instanceId?: string; featureCo
|
|||
operationKey,
|
||||
setDeletingMessages,
|
||||
setDeleteMessageError,
|
||||
() => deleteMessageApi(request, workflowId, messageId, apiBaseUrl),
|
||||
() => {
|
||||
if (!instanceId) throw new Error('instanceId required');
|
||||
return deleteMessageApi(request, instanceId, workflowId, messageId);
|
||||
},
|
||||
{
|
||||
default: 'Failed to delete message',
|
||||
notFound: 'Message not found or has already been deleted.',
|
||||
|
|
@ -569,7 +631,10 @@ export function useWorkflowOperations(options?: { instanceId?: string; featureCo
|
|||
operationKey,
|
||||
setDeletingFiles,
|
||||
setDeleteFileError,
|
||||
() => deleteFileFromMessageApi(request, workflowId, messageId, fileId, apiBaseUrl),
|
||||
() => {
|
||||
if (!instanceId) throw new Error('instanceId required');
|
||||
return deleteFileFromMessageApi(request, instanceId, workflowId, messageId, fileId);
|
||||
},
|
||||
{
|
||||
default: 'Failed to delete file',
|
||||
notFound: 'File not found or has already been deleted.',
|
||||
|
|
@ -583,7 +648,10 @@ export function useWorkflowOperations(options?: { instanceId?: string; featureCo
|
|||
setEditingWorkflows(prev => new Set(prev).add(workflowId));
|
||||
|
||||
try {
|
||||
const updatedWorkflow = await updateWorkflowApi(request, workflowId, updateData, apiBaseUrl);
|
||||
if (!instanceId) throw new Error('instanceId required');
|
||||
const updatedWorkflow = await updateWorkflowFromApi(request, instanceId, workflowId, {
|
||||
label: updateData.name,
|
||||
});
|
||||
|
||||
return { success: true, workflowData: updatedWorkflow };
|
||||
} catch (error: any) {
|
||||
|
|
|
|||
|
|
@ -11,16 +11,21 @@ import { useCurrentInstance } from '../hooks/useCurrentInstance';
|
|||
import { useFeaturesInitialized, useFeaturesLoading } from '../stores/featureStore';
|
||||
import styles from './FeatureLayout.module.css';
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
// =============================================================================
|
||||
// LOADING COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
const LoadingScreen: React.FC = () => (
|
||||
const LoadingScreen: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.loadingSpinner} />
|
||||
<p>Lade Feature-Daten...</p>
|
||||
<p>{t('ui.ladeFeaturedaten')}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// ERROR COMPONENT
|
||||
|
|
@ -31,16 +36,19 @@ interface ErrorScreenProps {
|
|||
returnPath?: string;
|
||||
}
|
||||
|
||||
const ErrorScreen: React.FC<ErrorScreenProps> = ({ message, returnPath = '/' }) => (
|
||||
const ErrorScreen: React.FC<ErrorScreenProps> = ({ message, returnPath = '/' }) => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className={styles.errorContainer}>
|
||||
<div className={styles.errorIcon}>⚠️</div>
|
||||
<h2>Zugriff nicht möglich</h2>
|
||||
<h2>{t('ui.zugriffNichtMoeglich')}</h2>
|
||||
<p>{message}</p>
|
||||
<a href={returnPath} className={styles.errorLink}>
|
||||
Zurück zur Übersicht
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// FEATURE LAYOUT
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import { CommcoachKeepAlive } from '../pages/views/commcoach/CommcoachKeepAlive'
|
|||
import { GraphicalEditorKeepAlive } from '../pages/views/graphicalEditor/GraphicalEditorKeepAlive';
|
||||
import styles from './MainLayout.module.css';
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
const _WORKSPACE_ROUTE_RE = /\/mandates\/[^/]+\/workspace\/[^/]+\/dashboard/;
|
||||
const _COMMCOACH_ROUTE_RE = /\/mandates\/[^/]+\/commcoach\/[^/]+\/(?:coaching|dossier)/;
|
||||
const _GE_EDITOR_ROUTE_RE = /\/mandates\/[^/]+\/graphicalEditor\/[^/]+\/editor/;
|
||||
|
|
@ -24,6 +26,8 @@ const _GE_EDITOR_ROUTE_RE = /\/mandates\/[^/]+\/graphicalEditor\/[^/]+\/editor/;
|
|||
// =============================================================================
|
||||
|
||||
const MainLayoutInner: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const { loadFeatures, initialized, loading, error } = useFeatureStore();
|
||||
const location = useLocation();
|
||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||
|
|
@ -60,7 +64,7 @@ const MainLayoutInner: React.FC = () => {
|
|||
<button
|
||||
className={styles.mobileBackdrop}
|
||||
onClick={() => setIsMobileSidebarOpen(false)}
|
||||
aria-label="Navigation schliessen"
|
||||
aria-label={t('mainLayout.navigationSchliessen')}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -102,7 +106,7 @@ const MainLayoutInner: React.FC = () => {
|
|||
<button
|
||||
className={styles.mobileMenuButton}
|
||||
onClick={() => setIsMobileSidebarOpen(true)}
|
||||
aria-label="Navigation oeffnen"
|
||||
aria-label={t('mainLayout.navigationOeffnen')}
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ export const AutomationsDashboardPage: React.FC = () => {
|
|||
|
||||
{metrics?.runsByStatus && Object.keys(metrics.runsByStatus).length > 0 && (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<h3 style={{ fontSize: '0.95rem', fontWeight: 600, marginBottom: 8 }}>Runs nach Status</h3>
|
||||
<h3 style={{ fontSize: '0.95rem', fontWeight: 600, marginBottom: 8 }}>{t('automationsDashboard.runsNachStatus')}</h3>
|
||||
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{Object.entries(metrics.runsByStatus).map(([status, count]) => (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import { FaArrowRight, FaBuilding } from 'react-icons/fa';
|
|||
import OnboardingAssistant from '../components/OnboardingAssistant';
|
||||
import styles from './Dashboard.module.css';
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
// =============================================================================
|
||||
// INSTANCE CARD
|
||||
// =============================================================================
|
||||
|
|
@ -53,6 +55,7 @@ const InstanceCard: React.FC<InstanceCardProps> = ({ instance, feature }) => {
|
|||
// =============================================================================
|
||||
|
||||
export const DashboardPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const { dynamicBlock, loading } = useNavigation();
|
||||
|
||||
// Alle Mandate und deren Features/Instanzen aus der Navigation
|
||||
|
|
@ -69,8 +72,8 @@ export const DashboardPage: React.FC = () => {
|
|||
return (
|
||||
<div className={styles.dashboard}>
|
||||
<header className={styles.header}>
|
||||
<h1>Übersicht</h1>
|
||||
<p className={styles.subtitle}>Lade...</p>
|
||||
<h1>{t('dashboard.uebersicht')}</h1>
|
||||
<p className={styles.subtitle}>{t('dashboard.lade')}</p>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -79,7 +82,7 @@ export const DashboardPage: React.FC = () => {
|
|||
return (
|
||||
<div className={styles.dashboard}>
|
||||
<header className={styles.header}>
|
||||
<h1>Übersicht</h1>
|
||||
<h1>{t('dashboard.uebersicht')}</h1>
|
||||
{totalInstances > 0 && (
|
||||
<p className={styles.subtitle}>
|
||||
Du hast Zugriff auf {totalInstances} Feature-Instanz{totalInstances !== 1 ? 'en' : ''} in {totalMandates} Mandant{totalMandates !== 1 ? 'en' : ''}.
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ import { CommcoachDashboardView, CommcoachDossierView, CommcoachSettingsView } f
|
|||
|
||||
import styles from './FeatureView.module.css';
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
// =============================================================================
|
||||
// PLACEHOLDER VIEWS (für nicht implementierte Features)
|
||||
// =============================================================================
|
||||
|
|
@ -63,39 +65,53 @@ const PlaceholderView: React.FC<{ title: string; description: string }> = ({ tit
|
|||
);
|
||||
|
||||
// Chatworkflow Views
|
||||
const ChatworkflowDashboard: React.FC = () => (
|
||||
<PlaceholderView title="Workflow Dashboard" description="Übersicht der Workflows" />
|
||||
const ChatworkflowDashboard: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<PlaceholderView title={t('feature.workflowDashboard')} description={t('feature.uebersichtDerWorkflows')} />
|
||||
);
|
||||
};
|
||||
|
||||
const ChatworkflowRuns: React.FC = () => (
|
||||
<PlaceholderView title="Runs" description="Workflow-Ausführungen" />
|
||||
);
|
||||
const ChatworkflowRuns: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
return <PlaceholderView title="Runs" description={t('feature.workflowausfuehrungen')} />;
|
||||
};
|
||||
|
||||
const ChatworkflowFiles: React.FC = () => (
|
||||
<PlaceholderView title="Dateien" description="Workflow-Dateien" />
|
||||
);
|
||||
const ChatworkflowFiles: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
return <PlaceholderView title={t('feature.dateien')} description="Workflow-Dateien" />;
|
||||
};
|
||||
|
||||
// Chatbot Views
|
||||
// ChatbotConversationsView is imported above
|
||||
|
||||
const ChatbotSettings: React.FC = () => (
|
||||
<PlaceholderView title="Chatbot Einstellungen" description="Konfiguration des Chatbots" />
|
||||
const ChatbotSettings: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<PlaceholderView title={t('feature.chatbotEinstellungen')} description={t('feature.konfigurationDesChatbots')} />
|
||||
);
|
||||
};
|
||||
|
||||
// Generic/Fallback
|
||||
const NotFound: React.FC = () => (
|
||||
const NotFound: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className={styles.notFound}>
|
||||
<h2>Seite nicht gefunden</h2>
|
||||
<p>Diese View existiert nicht oder wurde noch nicht implementiert.</p>
|
||||
<h2>{t('feature.seiteNichtGefunden')}</h2>
|
||||
<p>{t('feature.dieseViewExistiertNichtOder')}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AccessDenied: React.FC = () => (
|
||||
const AccessDenied: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
return (
|
||||
<div className={styles.accessDenied}>
|
||||
<h2>Zugriff verweigert</h2>
|
||||
<p>Du hast keine Berechtigung für diese Ansicht.</p>
|
||||
<h2>{t('feature.zugriffVerweigert')}</h2>
|
||||
<p>{t('feature.duHastKeineBerechtigungFuer')}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// VIEW REGISTRY
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import api from '../api';
|
|||
import { clearUserDataCache } from '../utils/userCache';
|
||||
import styles from './GDPR.module.css';
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
type ConsentInfo = {
|
||||
dataCollected?: Record<string, string>;
|
||||
dataProcessing?: Record<string, string>;
|
||||
|
|
@ -36,6 +38,7 @@ const downloadJson = (data: unknown, fileName: string, mimeType = 'application/j
|
|||
};
|
||||
|
||||
export const GDPRPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const contactEmail = 'p.motsch@poweron.swiss';
|
||||
const [consentInfo, setConsentInfo] = useState<ConsentInfo | null>(null);
|
||||
const [isLoadingConsent, setIsLoadingConsent] = useState(true);
|
||||
|
|
@ -158,11 +161,11 @@ export const GDPRPage: React.FC = () => {
|
|||
|
||||
<main className={styles.content}>
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Your data rights</h2>
|
||||
<h2 className={styles.sectionTitle}>{t('gDPR.yourDataRights')}</h2>
|
||||
<div className={styles.actions}>
|
||||
<div className={styles.actionCard}>
|
||||
<h3>Access (Article 15)</h3>
|
||||
<p>Download a full export of your account data.</p>
|
||||
<h3>{t('gDPR.accessArticle15')}</h3>
|
||||
<p>{t('gDPR.downloadAFullExportOf')}</p>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={handleDataExport}
|
||||
|
|
@ -183,8 +186,8 @@ export const GDPRPage: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className={styles.actionCard}>
|
||||
<h3>Portability (Article 20)</h3>
|
||||
<p>Download a machine-readable JSON-LD export.</p>
|
||||
<h3>{t('gDPR.portabilityArticle20')}</h3>
|
||||
<p>{t('gDPR.downloadAMachinereadableJsonldExport')}</p>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={handlePortabilityExport}
|
||||
|
|
@ -205,8 +208,8 @@ export const GDPRPage: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className={styles.actionCard}>
|
||||
<h3>Erasure (Article 17)</h3>
|
||||
<p>Permanently delete your account and all associated data.</p>
|
||||
<h3>{t('gDPR.erasureArticle17')}</h3>
|
||||
<p>{t('gDPR.permanentlyDeleteYourAccountAnd')}</p>
|
||||
{!showDeleteConfirm && (
|
||||
<button
|
||||
className={styles.dangerButton}
|
||||
|
|
@ -275,13 +278,13 @@ export const GDPRPage: React.FC = () => {
|
|||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Processing information</h2>
|
||||
{isLoadingConsent && <p className={styles.mutedText}>Loading consent info...</p>}
|
||||
<h2 className={styles.sectionTitle}>{t('gDPR.processingInformation')}</h2>
|
||||
{isLoadingConsent && <p className={styles.mutedText}>{t('gDPR.loadingConsentInfo')}</p>}
|
||||
{consentError && <p className={styles.errorText}>{consentError}</p>}
|
||||
{!isLoadingConsent && !consentError && consentInfo && (
|
||||
<div className={styles.infoGrid}>
|
||||
<div className={styles.infoBlock}>
|
||||
<h3>Data collected</h3>
|
||||
<h3>{t('gDPR.dataCollected')}</h3>
|
||||
<ul>
|
||||
{Object.entries(consentInfo.dataCollected || {}).map(([key, value]) => (
|
||||
<li key={key}>
|
||||
|
|
@ -291,7 +294,7 @@ export const GDPRPage: React.FC = () => {
|
|||
</ul>
|
||||
</div>
|
||||
<div className={styles.infoBlock}>
|
||||
<h3>Processing</h3>
|
||||
<h3>{t('gDPR.processing')}</h3>
|
||||
<ul>
|
||||
{Object.entries(consentInfo.dataProcessing || {}).map(([key, value]) => (
|
||||
<li key={key}>
|
||||
|
|
@ -301,7 +304,7 @@ export const GDPRPage: React.FC = () => {
|
|||
</ul>
|
||||
</div>
|
||||
<div className={styles.infoBlock}>
|
||||
<h3>Your rights</h3>
|
||||
<h3>{t('gDPR.yourRights')}</h3>
|
||||
<ul>
|
||||
{Object.entries(consentInfo.userRights || {}).map(([key, value]) => (
|
||||
<li key={key}>
|
||||
|
|
|
|||
|
|
@ -29,10 +29,14 @@ import { getUserDataCache } from '../utils/userCache';
|
|||
import { FaCheckCircle, FaTimesCircle, FaSpinner, FaSignInAlt, FaUserPlus } from 'react-icons/fa';
|
||||
import styles from './InvitePage.module.css';
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
// Key for storing pending invitation token
|
||||
export const PENDING_INVITATION_KEY = 'pendingInvitationToken';
|
||||
|
||||
export const InvitePage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const { token } = useParams<{ token: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { validateInvitation, acceptInvitation } = useInvitations();
|
||||
|
|
@ -53,7 +57,7 @@ export const InvitePage: React.FC = () => {
|
|||
useEffect(() => {
|
||||
const validate = async () => {
|
||||
if (!token) {
|
||||
setError('Kein Einladungs-Token angegeben');
|
||||
setError(t('invite.keinEinladungstokenAngegeben'));
|
||||
setValidating(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -159,7 +163,7 @@ export const InvitePage: React.FC = () => {
|
|||
<div className={styles.card}>
|
||||
<div className={styles.loading}>
|
||||
<FaSpinner className={styles.spinner} />
|
||||
<p>Einladung wird überprüft...</p>
|
||||
<p>{t('invite.einladungWirdUeberprueft')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -173,7 +177,7 @@ export const InvitePage: React.FC = () => {
|
|||
<div className={styles.card}>
|
||||
<div className={styles.errorState}>
|
||||
<FaTimesCircle className={styles.errorIcon} />
|
||||
<h1>Ungültige Einladung</h1>
|
||||
<h1>{t('invite.ungueltigeEinladung')}</h1>
|
||||
<p>{validation?.reason || 'Diese Einladung ist nicht gültig.'}</p>
|
||||
<Link to="/login" className={styles.primaryButton}>
|
||||
Zur Anmeldung
|
||||
|
|
@ -191,9 +195,9 @@ export const InvitePage: React.FC = () => {
|
|||
<div className={styles.card}>
|
||||
<div className={styles.successState}>
|
||||
<FaCheckCircle className={styles.successIcon} />
|
||||
<h1>Erfolgreich!</h1>
|
||||
<p>Sie wurden erfolgreich zum Mandanten hinzugefügt.</p>
|
||||
<p className={styles.redirectMessage}>Sie werden weitergeleitet...</p>
|
||||
<h1>{t('invite.erfolgreich')}</h1>
|
||||
<p>{t('invite.sieWurdenErfolgreichZumMandanten')}</p>
|
||||
<p className={styles.redirectMessage}>{t('invite.sieWerdenWeitergeleitet')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -208,12 +212,12 @@ export const InvitePage: React.FC = () => {
|
|||
<div className={styles.card}>
|
||||
<div className={styles.errorState}>
|
||||
<FaTimesCircle className={styles.errorIcon} />
|
||||
<h1>Falsche Anmeldung</h1>
|
||||
<h1>{t('invite.falscheAnmeldung')}</h1>
|
||||
<p>
|
||||
Diese Einladung ist für <strong>{validation.targetUsername}</strong> bestimmt.
|
||||
Sie sind als <strong>{cachedUser?.username || 'anderer Benutzer'}</strong> angemeldet.
|
||||
</p>
|
||||
<p>Bitte melden Sie sich ab und mit dem richtigen Konto wieder an.</p>
|
||||
<p>{t('invite.bitteMeldenSieSichAb')}</p>
|
||||
<Link to="/" className={styles.primaryButton}>
|
||||
Zum Dashboard
|
||||
</Link>
|
||||
|
|
@ -226,9 +230,9 @@ export const InvitePage: React.FC = () => {
|
|||
// Already authenticated - show accept button
|
||||
const isFeatureInvite = !!validation.featureInstanceId;
|
||||
const introText = isFeatureInvite
|
||||
? 'Sie wurden eingeladen, einem Mandanten und einem Feature beizutreten.'
|
||||
: 'Sie wurden eingeladen, einem Mandanten beizutreten.';
|
||||
const rolesLabel = isFeatureInvite ? 'Features mit zugewiesenen Rollen' : 'Zugewiesene Rollen';
|
||||
? t('invite.sieWurdenEingeladenEinemMandanten')
|
||||
: t('invite.sieWurdenEingeladenEinemMandanten');
|
||||
const rolesLabel = isFeatureInvite ? t('invite.featuresMitZugewiesenenRollen') : t('invite.zugewieseneRollen');
|
||||
const rolesValue = validation.featureInstanceName && validation.roleLabels?.length
|
||||
? `${validation.featureInstanceName} (${validation.roleLabels.join(', ')})`
|
||||
: validation.roleLabels?.join(', ') || '';
|
||||
|
|
@ -238,7 +242,7 @@ export const InvitePage: React.FC = () => {
|
|||
<div className={styles.container}>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<h1>Einladung annehmen</h1>
|
||||
<h1>{t('invite.einladungAnnehmen')}</h1>
|
||||
<p>{introText}</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -251,12 +255,12 @@ export const InvitePage: React.FC = () => {
|
|||
)}
|
||||
{validation.mandateName && (
|
||||
<div className={styles.infoRow}>
|
||||
<span className={styles.infoLabel}>Mandant:</span>
|
||||
<span className={styles.infoLabel}>{t('invite.mandant')}</span>
|
||||
<span className={styles.infoValue}>{validation.mandateName}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.infoRow}>
|
||||
<span className={styles.infoLabel}>Status:</span>
|
||||
<span className={styles.infoLabel}>{t('invite.status')}</span>
|
||||
<span className={styles.infoValue}>Angemeldet</span>
|
||||
</div>
|
||||
{rolesValue && (
|
||||
|
|
@ -301,7 +305,7 @@ export const InvitePage: React.FC = () => {
|
|||
<div className={styles.container}>
|
||||
<div className={styles.card}>
|
||||
<div className={styles.header}>
|
||||
<h1>Einladung annehmen</h1>
|
||||
<h1>{t('invite.einladungAnnehmen')}</h1>
|
||||
<p>{introText}</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -314,7 +318,7 @@ export const InvitePage: React.FC = () => {
|
|||
)}
|
||||
{validation.mandateName && (
|
||||
<div className={styles.infoRow}>
|
||||
<span className={styles.infoLabel}>Mandant:</span>
|
||||
<span className={styles.infoLabel}>{t('invite.mandant')}</span>
|
||||
<span className={styles.infoValue}>{validation.mandateName}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import OnboardingWizard from '../components/OnboardingWizard';
|
|||
import styles from './Login.module.css';
|
||||
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
function Login() {
|
||||
const { t } = useLanguage();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
// Pre-fill username from invitation if provided via location.state
|
||||
|
|
@ -142,7 +145,7 @@ function Login() {
|
|||
{hasPendingInvitation && (
|
||||
<div className={styles.invitationNotice}>
|
||||
<FaEnvelopeOpenText className={styles.invitationIcon} />
|
||||
<span>Sie haben eine ausstehende Einladung. Bitte melden Sie sich an, um diese anzunehmen.</span>
|
||||
<span>{t('login.sieHabenEineAusstehendeEinladung')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -183,7 +186,7 @@ function Login() {
|
|||
}}
|
||||
className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`}
|
||||
/>
|
||||
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>Passwort</label>
|
||||
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>{t('login.passwort')}</label>
|
||||
</div>
|
||||
<div className={styles.disclaimer}>
|
||||
<p>
|
||||
|
|
@ -234,7 +237,7 @@ function Login() {
|
|||
</button>
|
||||
|
||||
<div className={styles.registerLink}>
|
||||
<span>Du hast noch kein Konto?</span>
|
||||
<span>{t('login.duHastNochKeinKonto')}</span>
|
||||
</div>
|
||||
<div className={styles.ctaSection}>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ import styles from './PasswordResetRequest.module.css';
|
|||
import { usePasswordResetRequest } from '../hooks/useAuthentication';
|
||||
import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
function PasswordResetRequest() {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { requestReset, isLoading } = usePasswordResetRequest();
|
||||
const [username, setUsername] = useState('');
|
||||
|
|
@ -62,7 +66,7 @@ function PasswordResetRequest() {
|
|||
</div>
|
||||
<div className={styles.loginSection}>
|
||||
<div className={styles.loginBox}>
|
||||
<h2 className={styles.title}>Passwort zurücksetzen</h2>
|
||||
<h2 className={styles.title}>{t('passwordResetRequest.passwortZuruecksetzen')}</h2>
|
||||
<div className={styles.loginForm}>
|
||||
{validationError && (
|
||||
<div className={styles.error}>{validationError}</div>
|
||||
|
|
@ -97,7 +101,7 @@ function PasswordResetRequest() {
|
|||
</div>
|
||||
|
||||
<div className={styles.infoMessage}>
|
||||
<p>Geben Sie Ihren Benutzernamen ein. Falls ein Konto existiert, erhalten Sie einen Link zum Zurücksetzen des Passworts an Ihre hinterlegte E-Mail-Adresse.</p>
|
||||
<p>{t('passwordResetRequest.gebenSieIhrenBenutzernamenEin')}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
@ -111,7 +115,7 @@ function PasswordResetRequest() {
|
|||
)}
|
||||
|
||||
<div className={styles.registerLink}>
|
||||
<span>Zurück zum</span>
|
||||
<span>{t('passwordResetRequest.zurueckZum')}</span>
|
||||
<button
|
||||
className={styles.textButton}
|
||||
onClick={() => navigate("/login")}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { useRegister, useMsalRegister, useUsernameAvailability } from '../hooks/
|
|||
import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
|
||||
import { PENDING_INVITATION_KEY } from './InvitePage';
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
interface RegisterFormData {
|
||||
username: string;
|
||||
email: string;
|
||||
|
|
@ -14,6 +16,7 @@ interface RegisterFormData {
|
|||
}
|
||||
|
||||
function Register() {
|
||||
const { t } = useLanguage();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { register, error: registerError, isLoading } = useRegister();
|
||||
|
|
@ -135,7 +138,7 @@ function Register() {
|
|||
{hasPendingInvitation && !successMessage && (
|
||||
<div className={styles.invitationNotice}>
|
||||
<FaEnvelopeOpenText className={styles.invitationIcon} />
|
||||
<span>Sie haben eine ausstehende Einladung. Nach der Registrierung und Anmeldung können Sie diese annehmen.</span>
|
||||
<span>{t('register.sieHabenEineAusstehendeEinladung')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -188,11 +191,11 @@ function Register() {
|
|||
onBlur={() => setFullNameFocused(false)}
|
||||
className={`${styles.input} ${fullNameFocused || formData.fullName ? styles.focused : ''}`}
|
||||
/>
|
||||
<label className={fullNameFocused || formData.fullName ? styles.focusedLabel : styles.label}>Vollständiger Name</label>
|
||||
<label className={fullNameFocused || formData.fullName ? styles.focusedLabel : styles.label}>{t('register.vollstaendigerName')}</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.infoMessage}>
|
||||
<p>Nach der Registrierung erhalten Sie eine E-Mail mit einem Link zum Setzen Ihres Passworts.</p>
|
||||
<p>{t('register.nachDerRegistrierungErhaltenSie')}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.disclaimer}>
|
||||
|
|
@ -212,7 +215,7 @@ function Register() {
|
|||
)}
|
||||
|
||||
<div className={styles.registerLink}>
|
||||
<span>Bereits registriert?</span>
|
||||
<span>{t('register.bereitsRegistriert')}</span>
|
||||
<button
|
||||
className={styles.textButton}
|
||||
onClick={() => navigate("/login", { state: location.state })}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import styles from './Reset.module.css';
|
|||
import { usePasswordReset } from '../hooks/useAuthentication';
|
||||
import { generateAndStoreCSRFToken } from '../utils/csrfUtils';
|
||||
|
||||
import { useLanguage } from '../providers/language/LanguageContext';
|
||||
|
||||
function Reset() {
|
||||
const { t } = useLanguage();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { resetPassword, isLoading, error } = usePasswordReset();
|
||||
|
|
@ -104,7 +107,7 @@ function Reset() {
|
|||
</div>
|
||||
<div className={styles.loginSection}>
|
||||
<div className={styles.loginBox}>
|
||||
<h2 className={styles.title}>Neues Passwort setzen</h2>
|
||||
<h2 className={styles.title}>{t('reset.neuesPasswortSetzen')}</h2>
|
||||
<div className={styles.loginForm}>
|
||||
<div className={styles.error}>{tokenError}</div>
|
||||
<div className={styles.registerLink}>
|
||||
|
|
@ -116,7 +119,7 @@ function Reset() {
|
|||
</button>
|
||||
</div>
|
||||
<div className={styles.registerLink}>
|
||||
<span>oder zurück zum</span>
|
||||
<span>{t('reset.oderZurueckZum')}</span>
|
||||
<button
|
||||
className={styles.textButton}
|
||||
onClick={() => navigate("/login")}
|
||||
|
|
@ -144,7 +147,7 @@ function Reset() {
|
|||
</div>
|
||||
<div className={styles.loginSection}>
|
||||
<div className={styles.loginBox}>
|
||||
<h2 className={styles.title}>Neues Passwort setzen</h2>
|
||||
<h2 className={styles.title}>{t('reset.neuesPasswortSetzen')}</h2>
|
||||
<div className={styles.loginForm}>
|
||||
{(validationError || error) && (
|
||||
<div className={styles.error}>{validationError || error}</div>
|
||||
|
|
@ -156,7 +159,7 @@ function Reset() {
|
|||
|
||||
{!successMessage && (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className={styles.passwordHint}>Mindestens 8 Zeichen</div>
|
||||
<div className={styles.passwordHint}>{t('reset.mindestens8Zeichen')}</div>
|
||||
<div className={styles.floatingLabelInput}>
|
||||
<input
|
||||
type="password"
|
||||
|
|
@ -171,7 +174,7 @@ function Reset() {
|
|||
className={`${styles.input} ${passwordFocused || password ? styles.focused : ''}`}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>Neues Passwort</label>
|
||||
<label className={passwordFocused || password ? styles.focusedLabel : styles.label}>{t('reset.neuesPasswort')}</label>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -189,7 +192,7 @@ function Reset() {
|
|||
className={`${styles.input} ${confirmPasswordFocused || confirmPassword ? styles.focused : ''}`}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<label className={confirmPasswordFocused || confirmPassword ? styles.focusedLabel : styles.label}>Passwort bestätigen</label>
|
||||
<label className={confirmPasswordFocused || confirmPassword ? styles.focusedLabel : styles.label}>{t('reset.passwortBestaetigen')}</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
@ -203,7 +206,7 @@ function Reset() {
|
|||
)}
|
||||
|
||||
<div className={styles.registerLink}>
|
||||
<span>Zurück zum</span>
|
||||
<span>{t('reset.zurueckZum')}</span>
|
||||
<button
|
||||
className={styles.textButton}
|
||||
onClick={() => navigate("/login")}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,15 @@ import styles from './Settings.module.css';
|
|||
|
||||
type SettingsTab = 'profile' | 'appearance' | 'voice' | 'neutralization' | 'privacy';
|
||||
|
||||
const _TABS: { key: SettingsTab; label: string }[] = [
|
||||
{ key: 'profile', label: 'Profil' },
|
||||
{ key: 'appearance', label: 'Darstellung' },
|
||||
{ key: 'voice', label: 'Stimme & Sprache' },
|
||||
{ key: 'neutralization', label: 'Neutralisierung (lokal)' },
|
||||
{ key: 'privacy', label: 'Datenschutz' },
|
||||
function _getTabs(t: (key: string) => string): { key: SettingsTab; label: string }[] {
|
||||
return [
|
||||
{ key: 'profile', label: t('settings.tabProfil') },
|
||||
{ key: 'appearance', label: t('settings.tabDarstellung') },
|
||||
{ key: 'voice', label: t('settings.tabStimmeSprache') },
|
||||
{ key: 'neutralization', label: t('settings.tabNeutralisierung') },
|
||||
{ key: 'privacy', label: t('settings.tabDatenschutz') },
|
||||
];
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROFILE EDIT MODAL
|
||||
|
|
@ -41,21 +43,14 @@ interface ProfileEditModalProps {
|
|||
const ProfileEditModal: React.FC<ProfileEditModalProps> = ({ isOpen, onClose, userData, onSave }) => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { availableLanguages } = useLanguage();
|
||||
const { t, availableLanguages } = useLanguage();
|
||||
|
||||
const languageOptions =
|
||||
availableLanguages.length > 0
|
||||
? availableLanguages.map((l) => ({ value: l.code, label: l.label || l.code }))
|
||||
: [
|
||||
{ value: 'de', label: 'Deutsch' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'fr', label: 'Français' },
|
||||
];
|
||||
const languageOptions = availableLanguages.map((l) => ({ value: l.code, label: l.label || l.code }));
|
||||
|
||||
const profileAttributes: AttributeDefinition[] = [
|
||||
{ name: 'fullName', type: 'string', label: 'Vollstaendiger Name', description: 'Ihr vollstaendiger Name', required: false, placeholder: 'Max Mustermann' },
|
||||
{ name: 'email', type: 'email', label: 'E-Mail-Adresse', description: 'Ihre E-Mail-Adresse fuer Benachrichtigungen', required: true, placeholder: 'name@example.com' },
|
||||
{ name: 'language', type: 'select', label: 'Sprache', description: 'Anzeigesprache der Anwendung', required: true, options: languageOptions },
|
||||
{ name: 'fullName', type: 'string', label: t('settings.vollstaendigerName'), description: t('settings.ihrVollstaendigerName'), required: false, placeholder: t('settings.placeholderName') },
|
||||
{ name: 'email', type: 'email', label: t('settings.emailAdresse'), description: t('settings.emailBeschreibung'), required: true, placeholder: t('settings.placeholderEmail') },
|
||||
{ name: 'language', type: 'select', label: t('settings.sprache'), description: t('settings.anzeigespracheDerAnwendung'), required: true, options: languageOptions },
|
||||
];
|
||||
|
||||
const handleSubmit = async (formData: any) => {
|
||||
|
|
@ -77,12 +72,12 @@ const ProfileEditModal: React.FC<ProfileEditModalProps> = ({ isOpen, onClose, us
|
|||
<div className={styles.modalOverlay} onClick={onClose}>
|
||||
<div className={styles.modalContent} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2>Profil bearbeiten</h2>
|
||||
<h2>{t('settings.profilBearbeiten')}</h2>
|
||||
<button className={styles.closeButton} onClick={onClose}>×</button>
|
||||
</div>
|
||||
<div className={styles.modalBody}>
|
||||
{error && <div className={styles.errorMessage}>{error}</div>}
|
||||
<FormGeneratorForm attributes={profileAttributes} data={userData} mode="edit" onSubmit={handleSubmit} onCancel={onClose} submitButtonText={isSaving ? 'Speichern...' : 'Speichern'} cancelButtonText="Abbrechen" />
|
||||
<FormGeneratorForm attributes={profileAttributes} data={userData} mode="edit" onSubmit={handleSubmit} onCancel={onClose} submitButtonText={isSaving ? t('settings.speichern') : t('settings.speichern')} cancelButtonText={t('settings.abbrechen')} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -96,6 +91,7 @@ const ProfileEditModal: React.FC<ProfileEditModalProps> = ({ isOpen, onClose, us
|
|||
interface VoiceMapEntry { language: string; voiceName: string; }
|
||||
|
||||
const VoiceSettingsTab: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const { request } = useApiRequest();
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -181,7 +177,7 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
method: 'put',
|
||||
data: { sttLanguage, ttsLanguage: sttLanguage, ttsVoiceMap: mapObj },
|
||||
});
|
||||
setSuccess('Einstellungen gespeichert');
|
||||
setSuccess(t('settings.einstellungenGespeichert'));
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
await _loadSettings();
|
||||
} catch (err: any) {
|
||||
|
|
@ -203,7 +199,7 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
const audio = new Audio(`data:audio/mp3;base64,${result.audio}`);
|
||||
audio.play();
|
||||
}
|
||||
} catch { setError('Stimmtest fehlgeschlagen'); }
|
||||
} catch { setError(t('settings.stimmtestFehlgeschlagen')); }
|
||||
finally { setTesting(null); }
|
||||
}, [request]);
|
||||
|
||||
|
|
@ -219,7 +215,7 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
];
|
||||
const _displayLanguages = languages.length > 0 ? languages : _defaultLangs;
|
||||
|
||||
if (loading) return <div style={{ padding: '1rem', color: '#888' }}>Einstellungen werden geladen...</div>;
|
||||
if (loading) return <div style={{ padding: '1rem', color: '#888' }}>{t('settings.einstellungenWerdenGeladen')}</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -227,11 +223,11 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
{success && <div style={{ background: '#f0fdf4', border: '1px solid #bbf7d0', color: '#16a34a', padding: '0.75rem 1rem', borderRadius: 6, marginBottom: '1rem', fontSize: '0.875rem' }}>{success}</div>}
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>STT-Sprache (Spracheingabe)</h2>
|
||||
<h2 className={styles.sectionTitle}>{t('settings.sttspracheSpracheingabe')}</h2>
|
||||
<div className={styles.settingRow}>
|
||||
<div className={styles.settingInfo}>
|
||||
<label className={styles.settingLabel}>Sprache fuer Spracherkennung</label>
|
||||
<p className={styles.settingDescription}>Wird fuer die Sprache-zu-Text-Erkennung verwendet.</p>
|
||||
<label className={styles.settingLabel}>{t('settings.spracheFuerSpracherkennung')}</label>
|
||||
<p className={styles.settingDescription}>{t('settings.wirdFuerDieSprachezutexterkennungVerwendet')}</p>
|
||||
</div>
|
||||
<div className={styles.settingControl}>
|
||||
<select className={styles.select} value={sttLanguage} onChange={e => setSttLanguage(e.target.value)}>
|
||||
|
|
@ -244,7 +240,7 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>TTS-Stimmen (Sprachausgabe)</h2>
|
||||
<h2 className={styles.sectionTitle}>{t('settings.ttsstimmenSprachausgabe')}</h2>
|
||||
<p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
|
||||
Die Sprache wird automatisch erkannt. Hier kann pro Sprache eine bevorzugte Stimme festgelegt werden.
|
||||
</p>
|
||||
|
|
@ -255,7 +251,7 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
</div>
|
||||
) : (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
|
||||
<thead><tr style={{ borderBottom: '1px solid var(--border-color, #e0e0e0)' }}><th style={{ textAlign: 'left', padding: '0.5rem' }}>Sprache</th><th style={{ textAlign: 'left', padding: '0.5rem' }}>Stimme</th><th /><th /></tr></thead>
|
||||
<thead><tr style={{ borderBottom: '1px solid var(--border-color, #e0e0e0)' }}><th style={{ textAlign: 'left', padding: '0.5rem' }}>{t('settings.sprache')}</th><th style={{ textAlign: 'left', padding: '0.5rem' }}>{t('settings.stimme')}</th><th /><th /></tr></thead>
|
||||
<tbody>
|
||||
{voiceMap.map(entry => (
|
||||
<tr key={entry.language} style={{ borderBottom: '1px solid var(--border-color, #eee)' }}>
|
||||
|
|
@ -267,7 +263,7 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
</button>
|
||||
</td>
|
||||
<td style={{ padding: '0.5rem' }}>
|
||||
<button className={styles.button} style={{ padding: '0.25rem 0.5rem', fontSize: '0.75rem', color: '#dc2626' }} onClick={() => _handleRemoveEntry(entry.language)}>Entfernen</button>
|
||||
<button className={styles.button} style={{ padding: '0.25rem 0.5rem', fontSize: '0.75rem', color: '#dc2626' }} onClick={() => _handleRemoveEntry(entry.language)}>{t('settings.entfernen')}</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -277,7 +273,7 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
|
||||
<div style={{ marginTop: '1rem', display: 'flex', gap: '0.5rem', alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
||||
<div>
|
||||
<label className={styles.settingLabel} style={{ fontSize: '0.8rem' }}>Sprache</label>
|
||||
<label className={styles.settingLabel} style={{ fontSize: '0.8rem' }}>{t('settings.sprache')}</label>
|
||||
<select className={styles.select} value={addLanguage} onChange={e => setAddLanguage(e.target.value)}>
|
||||
{_displayLanguages.map((lang: any) => (
|
||||
<option key={lang.code || lang} value={lang.code || lang}>{lang.name || lang.code || lang}</option>
|
||||
|
|
@ -285,15 +281,15 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className={styles.settingLabel} style={{ fontSize: '0.8rem' }}>Stimme</label>
|
||||
<label className={styles.settingLabel} style={{ fontSize: '0.8rem' }}>{t('settings.stimme')}</label>
|
||||
<select className={styles.select} value={addVoiceName} onChange={e => setAddVoiceName(e.target.value)} disabled={loadingVoices}>
|
||||
<option value="">Standard</option>
|
||||
<option value="">{t('settings.standard')}</option>
|
||||
{addVoices.map((v: any) => (
|
||||
<option key={v.name || v} value={v.name || v}>{v.displayName || v.name || v}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button className={styles.button} onClick={_handleAddEntry} style={{ padding: '0.5rem 1rem' }}>Zuweisen</button>
|
||||
<button className={styles.button} onClick={_handleAddEntry} style={{ padding: '0.5rem 1rem' }}>{t('settings.zuweisen')}</button>
|
||||
<button className={styles.button} onClick={() => _handleTestVoice(addLanguage, addVoiceName)} disabled={testing !== null} style={{ padding: '0.5rem 1rem' }}>
|
||||
{testing === addLanguage ? '...' : 'Testen'}
|
||||
</button>
|
||||
|
|
@ -301,7 +297,7 @@ const VoiceSettingsTab: React.FC = () => {
|
|||
</section>
|
||||
|
||||
<button className={styles.button} onClick={_handleSave} disabled={saving} style={{ background: 'var(--primary-color, #2563eb)', color: '#fff', border: 'none', padding: '0.625rem 1.5rem', fontWeight: 600, borderRadius: 6 }}>
|
||||
{saving ? 'Speichern...' : 'Einstellungen speichern'}
|
||||
{saving ? t('settings.speichern') : t('settings.einstellungenSpeichern')}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
|
|
@ -320,6 +316,7 @@ interface NeutralizationMapping {
|
|||
}
|
||||
|
||||
const NeutralizationMappingsTab: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const { request } = useApiRequest();
|
||||
const [mappings, setMappings] = useState<NeutralizationMapping[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -361,14 +358,14 @@ const NeutralizationMappingsTab: React.FC = () => {
|
|||
return text.slice(0, 2) + '*'.repeat(Math.min(text.length - 4, 20)) + text.slice(-2);
|
||||
};
|
||||
|
||||
if (loading) return <div style={{ padding: '1rem', color: '#888' }}>Mappings werden geladen...</div>;
|
||||
if (loading) return <div style={{ padding: '1rem', color: '#888' }}>{t('settings.mappingsWerdenGeladen')}</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{error && <div className={styles.errorMessage}>{error}</div>}
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Platzhalter-Mappings (lokal)</h2>
|
||||
<h2 className={styles.sectionTitle}>{t('settings.platzhaltermappingsLokal')}</h2>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '1rem',
|
||||
|
|
@ -382,7 +379,7 @@ const NeutralizationMappingsTab: React.FC = () => {
|
|||
}}
|
||||
>
|
||||
<strong>AI-Workspace:</strong> Neutralisierter Chat-Text, Dokumente und Platzhalter-Mappings finden Sie unter{' '}
|
||||
<strong>Mandant → AI-Workspace-Instanz → Einstellungen → Tab „Neutralisierung“</strong> (nicht auf dieser
|
||||
<strong>{t('settings.mandantAiworkspaceinstanzEinstellungenTabNeutralisierung')}</strong> (nicht auf dieser
|
||||
Seite). Dieser Tab zeigt nur die <strong>lokale</strong> Liste über <code>/api/local/neutralization-mappings</code>.
|
||||
</div>
|
||||
<p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
|
||||
|
|
@ -439,7 +436,7 @@ const NeutralizationMappingsTab: React.FC = () => {
|
|||
// =============================================================================
|
||||
|
||||
export const SettingsPage: React.FC = () => {
|
||||
const { currentLanguage, setLanguage, availableLanguages, refreshAvailableLanguages } = useLanguage();
|
||||
const { t, currentLanguage, setLanguage, availableLanguages, refreshAvailableLanguages } = useLanguage();
|
||||
const { user: currentUser, refetch: refetchUser } = useCurrentUser();
|
||||
const { updateUser } = useUser();
|
||||
|
||||
|
|
@ -491,12 +488,12 @@ export const SettingsPage: React.FC = () => {
|
|||
return (
|
||||
<div className={styles.settings}>
|
||||
<header className={styles.header}>
|
||||
<h1>Einstellungen</h1>
|
||||
<p className={styles.subtitle}>Persoenliche Einstellungen und Praeferenzen</p>
|
||||
<h1>{t('settings.einstellungen')}</h1>
|
||||
<p className={styles.subtitle}>{t('settings.persoenlicheEinstellungenUndPraeferenzen')}</p>
|
||||
</header>
|
||||
|
||||
<nav style={{ display: 'flex', gap: 0, borderBottom: '1px solid var(--border-color, #e0e0e0)', marginBottom: '1.5rem' }}>
|
||||
{_TABS.map(tab => (
|
||||
{_getTabs(t).map(tab => (
|
||||
<button key={tab.key} onClick={() => setActiveTab(tab.key)} style={{
|
||||
padding: '10px 20px', border: 'none', borderBottom: activeTab === tab.key ? '2px solid var(--primary-color, #2563eb)' : '2px solid transparent',
|
||||
background: 'none', cursor: 'pointer', fontSize: 14, fontWeight: activeTab === tab.key ? 600 : 400,
|
||||
|
|
@ -511,14 +508,14 @@ export const SettingsPage: React.FC = () => {
|
|||
{activeTab === 'profile' && (
|
||||
<>
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Konto</h2>
|
||||
<h2 className={styles.sectionTitle}>{t('settings.konto')}</h2>
|
||||
<div className={styles.settingRow}>
|
||||
<div className={styles.settingInfo}>
|
||||
<label className={styles.settingLabel}>Profil bearbeiten</label>
|
||||
<p className={styles.settingDescription}>Aendern Sie Ihren Namen und Ihre E-Mail-Adresse.</p>
|
||||
<label className={styles.settingLabel}>{t('settings.profilBearbeiten')}</label>
|
||||
<p className={styles.settingDescription}>{t('settings.aendernSieIhrenNamenUnd')}</p>
|
||||
</div>
|
||||
<div className={styles.settingControl}>
|
||||
<button className={styles.button} onClick={async () => { await refetchUser(); setIsProfileModalOpen(true); }}>Profil oeffnen</button>
|
||||
<button className={styles.button} onClick={async () => { await refetchUser(); setIsProfileModalOpen(true); }}>{t('settings.profilOeffnen')}</button>
|
||||
</div>
|
||||
</div>
|
||||
{currentUser && (
|
||||
|
|
@ -541,34 +538,27 @@ export const SettingsPage: React.FC = () => {
|
|||
|
||||
{activeTab === 'appearance' && (
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Darstellung</h2>
|
||||
<h2 className={styles.sectionTitle}>{t('settings.darstellung')}</h2>
|
||||
<div className={styles.settingRow}>
|
||||
<div className={styles.settingInfo}><label className={styles.settingLabel}>Theme</label><p className={styles.settingDescription}>Waehlen Sie zwischen hellem und dunklem Design.</p></div>
|
||||
<div className={styles.settingInfo}><label className={styles.settingLabel}>Theme</label><p className={styles.settingDescription}>{t('settings.waehlenSieZwischenHellemUnd')}</p></div>
|
||||
<div className={styles.settingControl}>
|
||||
<div className={styles.themeToggle}>
|
||||
<button className={`${styles.themeButton} ${theme === 'light' ? styles.active : ''}`} onClick={() => handleThemeChange('light')}>Hell</button>
|
||||
<button className={`${styles.themeButton} ${theme === 'dark' ? styles.active : ''}`} onClick={() => handleThemeChange('dark')}>Dunkel</button>
|
||||
<button className={`${styles.themeButton} ${theme === 'light' ? styles.active : ''}`} onClick={() => handleThemeChange('light')}>{t('settings.themeHell')}</button>
|
||||
<button className={`${styles.themeButton} ${theme === 'dark' ? styles.active : ''}`} onClick={() => handleThemeChange('dark')}>{t('settings.themeDunkel')}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.settingRow}>
|
||||
<div className={styles.settingInfo}><label className={styles.settingLabel}>Anzeigesprache</label><p className={styles.settingDescription}>Waehlen Sie die Sprache der Benutzeroberflaeche.{languageError && <span className={styles.errorText}> {languageError}</span>}</p></div>
|
||||
<div className={styles.settingInfo}><label className={styles.settingLabel}>{t('settings.anzeigesprache')}</label><p className={styles.settingDescription}>{t('settings.spracheBeschreibung')}{languageError && <span className={styles.errorText}> {languageError}</span>}</p></div>
|
||||
<div className={styles.settingControl}>
|
||||
<select className={styles.select} value={currentLanguage} onChange={(e) => handleLanguageChange(e.target.value)} disabled={isSavingLanguage}>
|
||||
{(availableLanguages.length > 0
|
||||
? availableLanguages
|
||||
: [
|
||||
{ code: 'de', label: 'Deutsch' },
|
||||
{ code: 'en', label: 'English' },
|
||||
{ code: 'fr', label: 'Français' },
|
||||
]
|
||||
).map((l) => (
|
||||
{availableLanguages.map((l) => (
|
||||
<option key={l.code} value={l.code}>
|
||||
{l.label || l.code}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isSavingLanguage && <span className={styles.savingIndicator}>Speichern...</span>}
|
||||
{isSavingLanguage && <span className={styles.savingIndicator}>{t('settings.speichern')}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -580,15 +570,13 @@ export const SettingsPage: React.FC = () => {
|
|||
|
||||
{activeTab === 'privacy' && (
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Datenschutz</h2>
|
||||
<h2 className={styles.sectionTitle}>{t('settings.datenschutz')}</h2>
|
||||
<p className={styles.settingDescription} style={{ marginBottom: '1rem' }}>
|
||||
Feature-Daten (z. B. Workspace, CommCoach, Automation) liegen mandantenbezogen; Zugriff richtet sich
|
||||
nach Ihren Rollen im Mandanten und an Feature-Instanzen. Allgemeine Rechte (Auskunft, Export,
|
||||
Löschung) finden Sie unter GDPR.
|
||||
{t('settings.datenschutzBeschreibung')}
|
||||
</p>
|
||||
<div className={styles.settingRow}>
|
||||
<div className={styles.settingInfo}><label className={styles.settingLabel}>GDPR / Privacy</label><p className={styles.settingDescription}>Datenexport, Portabilität und Kontolöschung.</p></div>
|
||||
<div className={styles.settingControl}><Link className={`${styles.button} ${styles.linkButton}`} to="/gdpr">GDPR öffnen</Link></div>
|
||||
<div className={styles.settingInfo}><label className={styles.settingLabel}>{t('settings.gdprPrivacy')}</label><p className={styles.settingDescription}>{t('settings.datenexportPortabilitaetUndKontoloeschung')}</p></div>
|
||||
<div className={styles.settingControl}><Link className={`${styles.button} ${styles.linkButton}`} to="/gdpr">{t('settings.gdprOeffnen')}</Link></div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ const FeatureCard: React.FC<FeatureCardProps> = ({
|
|||
onActivate,
|
||||
onDeactivate,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const isProcessing = actionLoading === feature.featureCode;
|
||||
const icon = FEATURE_ICONS[feature.featureCode];
|
||||
const activeInstances = feature.instances.filter(inst => inst.isActive);
|
||||
|
|
@ -137,7 +138,7 @@ const FeatureCard: React.FC<FeatureCardProps> = ({
|
|||
disabled={isProcessing}
|
||||
>
|
||||
{isProcessing
|
||||
? (language === 'de' ? 'Wird aktiviert...' : 'Activating...')
|
||||
? (language === 'de' ? t('store.wirdAktiviert') : t('store.activating'))
|
||||
: (language === 'de'
|
||||
? `Aktivieren fuer ${m.label || m.name}`
|
||||
: language === 'fr'
|
||||
|
|
@ -151,19 +152,19 @@ const FeatureCard: React.FC<FeatureCardProps> = ({
|
|||
};
|
||||
|
||||
const StorePage: React.FC = () => {
|
||||
const { currentLanguage } = useLanguage();
|
||||
const { t, currentLanguage } = useLanguage();
|
||||
const { features, mandates, subscriptionInfo, loading, actionLoading, error, activate, deactivate } = useStore();
|
||||
|
||||
return (
|
||||
<div className={styles.store}>
|
||||
<div className={styles.header}>
|
||||
<h1>{currentLanguage === 'de' ? 'Feature Store' : currentLanguage === 'fr' ? 'Feature Store' : 'Feature Store'}</h1>
|
||||
<h1>{currentLanguage === 'de' ? 'Feature Store' : currentLanguage === 'fr' ? t('store.featureStore') : t('store.featureStore')}</h1>
|
||||
<p className={styles.subtitle}>
|
||||
{currentLanguage === 'de'
|
||||
? 'Aktiviere Features fuer dein Konto. Deine Daten sind isoliert und nur fuer dich sichtbar.'
|
||||
: currentLanguage === 'fr'
|
||||
? 'Activez des fonctionnalites pour votre compte. Vos donnees sont isolees et visibles uniquement par vous.'
|
||||
: 'Activate features for your account. Your data is isolated and only visible to you.'}
|
||||
? t('store.activezDesFonctionnalitesPourVotre')
|
||||
: t('store.activateFeaturesForYourAccount')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -188,7 +189,7 @@ const StorePage: React.FC = () => {
|
|||
)}
|
||||
{subscriptionInfo.status === 'TRIALING' && subscriptionInfo.trialEndsAt && (
|
||||
<span className={styles.bannerSeparator}>
|
||||
{currentLanguage === 'de' ? 'Trial endet' : 'Trial ends'}: {new Date(subscriptionInfo.trialEndsAt).toLocaleDateString()}
|
||||
{currentLanguage === 'de' ? t('store.trialEndet') : t('store.trialEnds')}: {new Date(subscriptionInfo.trialEndsAt).toLocaleDateString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -198,13 +199,13 @@ const StorePage: React.FC = () => {
|
|||
|
||||
{loading ? (
|
||||
<div className={styles.loading}>
|
||||
{currentLanguage === 'de' ? 'Lade Features...' : 'Loading features...'}
|
||||
{currentLanguage === 'de' ? t('store.ladeFeatures') : t('store.loadingFeatures')}
|
||||
</div>
|
||||
) : features.length === 0 ? (
|
||||
<div className={styles.empty}>
|
||||
{currentLanguage === 'de'
|
||||
? 'Keine Features im Store verfuegbar.'
|
||||
: 'No features available in the store.'}
|
||||
? t('store.keineFeaturesImStoreVerfuegbar')
|
||||
: t('store.noFeaturesAvailableInThe')}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.grid}>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import { InstanceDetailModal } from './InstanceDetailModal';
|
|||
import { FeatureInstanceWizard } from './wizards/FeatureInstanceWizard';
|
||||
import { InstanceHierarchyView } from './InstanceHierarchyView';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
function getMandateName(mandate: Mandate): string {
|
||||
if (mandate.label) return mandate.label;
|
||||
if (typeof mandate.name === 'object') {
|
||||
|
|
@ -44,6 +46,8 @@ export interface InstanceWithStats extends FeatureInstance {
|
|||
}
|
||||
|
||||
export const AccessManagementHub: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const {
|
||||
features,
|
||||
instances,
|
||||
|
|
@ -339,7 +343,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
value={selectedMandateId}
|
||||
onChange={(e) => setSelectedMandateId(e.target.value)}
|
||||
>
|
||||
<option value="">-- Mandant wählen --</option>
|
||||
<option value="">{t('accessManagementHub.mandantWaehlen')}</option>
|
||||
{mandates.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{getMandateName(m)}
|
||||
|
|
@ -357,7 +361,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
value={selectedFeatureCode}
|
||||
onChange={(e) => setSelectedFeatureCode(e.target.value)}
|
||||
>
|
||||
<option value="">Alle</option>
|
||||
<option value="">{t('accessManagementHub.alle')}</option>
|
||||
{features.map((f) => (
|
||||
<option key={f.code} value={f.code}>
|
||||
{getFeatureLabel(f)}
|
||||
|
|
@ -428,7 +432,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
) : !selectedMandateId ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Kein Mandant ausgewählt</h3>
|
||||
<h3 className={styles.emptyTitle}>{t('accessManagementHub.keinMandantAusgewaehlt')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Wählen Sie einen Mandanten, um dessen Feature-Instanzen und Zugriffe zu verwalten.
|
||||
</p>
|
||||
|
|
@ -451,7 +455,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
<span className={hubStyles.statsValue}>
|
||||
{loading || statsLoading ? '…' : overviewStats.users}
|
||||
</span>
|
||||
<span className={hubStyles.statsLabel}>Benutzer</span>
|
||||
<span className={hubStyles.statsLabel}>{t('accessManagementHub.benutzer')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={hubStyles.statsCard}>
|
||||
|
|
@ -460,7 +464,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
<span className={hubStyles.statsValue}>
|
||||
{loading || statsLoading ? '…' : overviewStats.roles}
|
||||
</span>
|
||||
<span className={hubStyles.statsLabel}>Rollen (max)</span>
|
||||
<span className={hubStyles.statsLabel}>{t('accessManagementHub.rollenMax')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{relationshipData && relationshipData.instances.length > 0 && (
|
||||
|
|
@ -493,12 +497,12 @@ export const AccessManagementHub: React.FC = () => {
|
|||
{loading && filteredInstances.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Instanzen...</span>
|
||||
<span>{t('accessManagementHub.ladeInstanzen')}</span>
|
||||
</div>
|
||||
) : filteredInstances.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaCube className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Keine Feature-Instanzen</h3>
|
||||
<h3 className={styles.emptyTitle}>{t('accessManagementHub.keineFeatureinstanzen')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Erstellen Sie eine neue Instanz oder wählen Sie ein anderes Feature.
|
||||
</p>
|
||||
|
|
@ -519,7 +523,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
<span
|
||||
className={`${hubStyles.instanceBadge} ${inst.enabled ? hubStyles.badgeActive : hubStyles.badgeInactive}`}
|
||||
>
|
||||
{inst.enabled ? 'Aktiv' : 'Inaktiv'}
|
||||
{inst.enabled ? t('accessManagementHub.aktiv') : t('accessManagementHub.inaktiv')}
|
||||
</span>
|
||||
</div>
|
||||
<div className={hubStyles.instanceMeta}>
|
||||
|
|
@ -540,7 +544,7 @@ export const AccessManagementHub: React.FC = () => {
|
|||
className={hubStyles.cardAction}
|
||||
onClick={() => handleSyncRoles(inst)}
|
||||
disabled={!inst.enabled}
|
||||
title="Rollen synchronisieren"
|
||||
title={t('accessManagementHub.rollenSynchronisieren')}
|
||||
>
|
||||
<FaCogs /> Rollen sync
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@ import { DropdownSelect } from '../../components/UiComponents/DropdownSelect';
|
|||
import { TextField } from '../../components/UiComponents/TextField';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
export const AdminFeatureAccessPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const {
|
||||
features,
|
||||
instances,
|
||||
|
|
@ -84,8 +88,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
|
||||
// Table columns
|
||||
const columns = useMemo(() => [
|
||||
{ key: 'label', label: 'Name', type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 200 },
|
||||
{ key: 'featureCode', label: 'Feature', type: 'string' as const, sortable: true, filterable: true, width: 150,
|
||||
{ key: 'label', label: t('adminFeatureAccess.name'), type: 'string' as const, sortable: true, filterable: true, searchable: true, width: 200 },
|
||||
{ key: 'featureCode', label: t('adminFeatureAccess.feature'), type: 'string' as const, sortable: true, filterable: true, width: 150,
|
||||
render: (value: string) => {
|
||||
const feature = features.find(f => f.code === value);
|
||||
if (feature) {
|
||||
|
|
@ -97,8 +101,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
return value;
|
||||
}
|
||||
},
|
||||
{ key: 'enabled', label: 'Aktiv', type: 'boolean' as const, sortable: true, filterable: true, width: 80 },
|
||||
], [features]);
|
||||
{ key: 'enabled', label: t('adminFeatureAccess.aktiv'), type: 'boolean' as const, sortable: true, filterable: true, width: 80 },
|
||||
], [features, t]);
|
||||
|
||||
// Form attributes from backend - merge with dynamic feature options
|
||||
// Exclude featureCode, config, and label since we handle them separately
|
||||
|
|
@ -349,7 +353,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Feature-Instanzen</h1>
|
||||
<p className={styles.pageSubtitle}>Verwalten Sie Feature-Instanzen für jeden Mandanten</p>
|
||||
<p className={styles.pageSubtitle}>{t('adminFeatureAccess.verwaltenSieFeatureinstanzenFuerJeden')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -365,7 +369,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
value={selectedMandateId}
|
||||
onChange={(e) => setSelectedMandateId(e.target.value)}
|
||||
>
|
||||
<option value="">-- Mandant wählen --</option>
|
||||
<option value="">{t('adminFeatureAccess.mandantWaehlen')}</option>
|
||||
{mandates.map(m => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{getMandateName(m)}
|
||||
|
|
@ -399,7 +403,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
{features.length > 0 ? (
|
||||
<div className={styles.infoBox}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
<span>Verfügbare Features: </span>
|
||||
<span>{t('adminFeatureAccess.verfuegbareFeatures')} </span>
|
||||
{features.map((f, i) => (
|
||||
<span key={f.code}>
|
||||
{i > 0 && ', '}
|
||||
|
|
@ -429,7 +433,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
{!selectedMandateId ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Kein Mandant ausgewählt</h3>
|
||||
<h3 className={styles.emptyTitle}>{t('adminFeatureAccess.keinMandantAusgewaehlt')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Wählen Sie einen Mandanten aus, um dessen Feature-Instanzen zu verwalten.
|
||||
</p>
|
||||
|
|
@ -450,7 +454,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
actionButtons={[
|
||||
{
|
||||
type: 'delete' as const,
|
||||
title: 'Instanz löschen',
|
||||
title: t('adminFeatureAccessPage.deleteInstance'),
|
||||
}
|
||||
]}
|
||||
customActions={[
|
||||
|
|
@ -458,13 +462,13 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
id: 'edit',
|
||||
icon: <FaEdit />,
|
||||
onClick: handleEditClick,
|
||||
title: 'Instanz bearbeiten',
|
||||
title: t('adminFeatureAccessPage.editInstance'),
|
||||
},
|
||||
{
|
||||
id: 'syncRoles',
|
||||
icon: <FaCogs />,
|
||||
onClick: handleSyncRoles,
|
||||
title: 'Rollen synchronisieren',
|
||||
title: t('adminFeatureAccessPage.syncRoles'),
|
||||
loading: (row: FeatureInstance) => syncingInstance === row.id,
|
||||
disabled: (row: FeatureInstance) => !row.enabled,
|
||||
}
|
||||
|
|
@ -474,7 +478,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
pagination: instancesPagination,
|
||||
handleDelete: handleDeleteInstance,
|
||||
}}
|
||||
emptyMessage="Keine Feature-Instanzen gefunden"
|
||||
emptyMessage={t('adminFeatureAccess.keineFeatureinstanzenGefunden')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -484,7 +488,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Neue Feature-Instanz erstellen</h2>
|
||||
<h2 className={styles.modalTitle}>{t('adminFeatureAccess.neueFeatureinstanzErstellen')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
|
|
@ -494,11 +498,11 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
{features.length === 0 ? (
|
||||
<p>Keine Features verfügbar. Bitte wenden Sie sich an den System-Administrator.</p>
|
||||
<p>{t('adminFeatureAccess.keineFeaturesVerfuegbarBitteWenden')}</p>
|
||||
) : createFields.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Formular...</span>
|
||||
<span>{t('adminFeatureAccess.ladeFormular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
|
|
@ -525,7 +529,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
setChatbotEnableWebResearch(true);
|
||||
setChatbotAllowedProviders([]);
|
||||
}}
|
||||
placeholder="Feature auswählen (erforderlich)"
|
||||
placeholder={t('adminFeatureAccess.featureAuswaehlenErforderlich')}
|
||||
className={styles.configSelect}
|
||||
/>
|
||||
{!createFeatureCode && (
|
||||
|
|
@ -552,7 +556,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
type="text"
|
||||
value={createLabel}
|
||||
onChange={(value) => setCreateLabel(value)}
|
||||
placeholder="Instanz-Bezeichnung eingeben..."
|
||||
placeholder={t('adminFeatureAccess.instanzbezeichnungEingeben')}
|
||||
className={styles.configSelect}
|
||||
size="md"
|
||||
required={true}
|
||||
|
|
@ -591,8 +595,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
setChatbotEnableWebResearch(true);
|
||||
setChatbotAllowedProviders([]);
|
||||
}}
|
||||
submitButtonText="Erstellen"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('adminFeatureAccess.erstellen')}
|
||||
cancelButtonText={t('adminFeatureAccess.abbrechen')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -608,7 +612,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
<div className={styles.modalOverlay} onClick={() => { setShowEditModal(false); setEditingInstance(null); }}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Feature-Instanz bearbeiten</h2>
|
||||
<h2 className={styles.modalTitle}>{t('adminFeatureAccess.featureinstanzBearbeiten')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => { setShowEditModal(false); setEditingInstance(null); }}
|
||||
|
|
@ -622,14 +626,14 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
{
|
||||
name: 'label',
|
||||
type: 'string' as const,
|
||||
label: 'Bezeichnung',
|
||||
label: t('adminFeatureAccessPage.bezeichnung'),
|
||||
required: true,
|
||||
editable: true,
|
||||
},
|
||||
{
|
||||
name: 'enabled',
|
||||
type: 'boolean' as const,
|
||||
label: 'Aktiviert',
|
||||
label: t('adminFeatureAccessPage.aktiviert'),
|
||||
required: false,
|
||||
editable: true,
|
||||
}
|
||||
|
|
@ -645,8 +649,8 @@ export const AdminFeatureAccessPage: React.FC = () => {
|
|||
setChatbotEnableWebResearch(true);
|
||||
setChatbotAllowedProviders([]);
|
||||
}}
|
||||
submitButtonText="Speichern"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('adminFeatureAccess.speichern')}
|
||||
cancelButtonText={t('adminFeatureAccess.abbrechen')}
|
||||
/>
|
||||
|
||||
{/* Chatbot Configuration Section */}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,17 @@ import { useFeatureAccess, type FeatureAccessUser, type FeatureInstanceRole, typ
|
|||
import { useUserMandates } from '../../hooks/useUserMandates';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaPlus, FaSync, FaUsers, FaBuilding, FaCube } from 'react-icons/fa';
|
||||
import { FaPlus, FaSync, FaBuilding, FaCube } from 'react-icons/fa';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import { useFeatureStore } from '../../stores/featureStore';
|
||||
import api from '../../api';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const {
|
||||
features,
|
||||
instances,
|
||||
|
|
@ -198,7 +202,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
const columns = useMemo(() => [
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Benutzername',
|
||||
label: t('adminFeatureInstanceUsers.benutzername'),
|
||||
type: 'text' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -207,7 +211,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: 'E-Mail',
|
||||
label: t('adminFeatureInstanceUsers.email'),
|
||||
type: 'text' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -216,7 +220,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'fullName',
|
||||
label: 'Vollständiger Name',
|
||||
label: t('adminFeatureInstanceUsers.vollstaendigerName'),
|
||||
type: 'text' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -225,7 +229,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'roleLabels',
|
||||
label: 'Rollen',
|
||||
label: t('adminFeatureInstanceUsers.rollen'),
|
||||
type: 'text' as const,
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
|
|
@ -238,14 +242,14 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
label: 'Aktiv',
|
||||
label: t('adminFeatureInstanceUsers.aktiv'),
|
||||
type: 'boolean' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
searchable: false,
|
||||
width: 80,
|
||||
},
|
||||
], []);
|
||||
], [t]);
|
||||
|
||||
// Dynamic options for forms (users and roles)
|
||||
const userOptions = useMemo(() =>
|
||||
|
|
@ -265,39 +269,39 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
return [
|
||||
{
|
||||
name: 'userId',
|
||||
label: 'Benutzer',
|
||||
label: t('adminFeatureInstanceUsersPage.benutzer'),
|
||||
type: 'enum' as const,
|
||||
required: true,
|
||||
options: userOptions,
|
||||
},
|
||||
{
|
||||
name: 'roleIds',
|
||||
label: 'Rollen',
|
||||
label: t('adminFeatureInstanceUsersPage.rollen'),
|
||||
type: 'multiselect' as const,
|
||||
required: true,
|
||||
options: roleOptions,
|
||||
}
|
||||
];
|
||||
}, [userOptions, roleOptions]);
|
||||
}, [userOptions, roleOptions, t]);
|
||||
|
||||
// Form attributes for editing user roles and active flag
|
||||
const editRolesFields: AttributeDefinition[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'roleIds',
|
||||
label: 'Rollen',
|
||||
label: t('adminFeatureInstanceUsersPage.rollen'),
|
||||
type: 'multiselect' as const,
|
||||
required: true,
|
||||
options: roleOptions,
|
||||
},
|
||||
{
|
||||
name: 'enabled',
|
||||
label: 'Aktiv',
|
||||
label: t('adminFeatureInstanceUsersPage.aktiv'),
|
||||
type: 'checkbox' as const,
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
}, [roleOptions]);
|
||||
}, [roleOptions, t]);
|
||||
|
||||
// Handle add user submit
|
||||
const handleAddUser = async (data: { userId: string; roleIds: string[] }) => {
|
||||
|
|
@ -406,8 +410,8 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Feature Instanz Benutzer</h1>
|
||||
<p className={styles.pageSubtitle}>Verwalten Sie Benutzerzugriffe auf Feature-Instanzen</p>
|
||||
<h1 className={styles.pageTitle}>{t('adminFeatureInstanceUsers.featureInstanzBenutzer')}</h1>
|
||||
<p className={styles.pageSubtitle}>{t('adminFeatureInstanceUsers.verwaltenSieBenutzerzugriffeAufFeatureinstanzen')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -424,7 +428,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
onChange={(e) => setSelectedCombinedKey(e.target.value)}
|
||||
disabled={loading || combinedOptions.length === 0}
|
||||
>
|
||||
<option value="">-- Mandant / Feature-Instanz wählen --</option>
|
||||
<option value="">{t('adminFeatureInstanceUsers.mandantFeatureinstanzWaehlen')}</option>
|
||||
{/* Group options by mandate */}
|
||||
{(() => {
|
||||
const groupedByMandate: Record<string, CombinedInstanceOption[]> = {};
|
||||
|
|
@ -482,7 +486,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
{/* Roles info box */}
|
||||
{selectedInstance && instanceRoles.length > 0 && (
|
||||
<div className={styles.infoBox}>
|
||||
<span>Verfügbare Rollen: </span>
|
||||
<span>{t('adminFeatureInstanceUsers.verfuegbareRollen')} </span>
|
||||
{instanceRoles.map((r, i) => (
|
||||
<span key={r.id}>
|
||||
{i > 0 && ', '}
|
||||
|
|
@ -496,7 +500,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
{selectedInstance && instanceRoles.length === 0 && !usersLoading && (
|
||||
<div className={styles.warningBox || styles.infoBox}>
|
||||
<span>⚠️ </span>
|
||||
<span>Diese Instanz hat noch keine Rollen. Bitte synchronisieren Sie die Rollen zuerst unter "Feature-Instanzen".</span>
|
||||
<span>{t('adminFeatureInstanceUsers.dieseInstanzHatNochKeine')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -504,11 +508,11 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
{!selectedCombinedKey ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaCube className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Keine Feature-Instanz ausgewählt</h3>
|
||||
<h3 className={styles.emptyTitle}>{t('adminFeatureInstanceUsers.keineFeatureinstanzAusgewaehlt')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
{combinedOptions.length === 0
|
||||
? 'Es gibt noch keine Feature-Instanzen. Erstellen Sie zuerst Feature-Instanzen unter "Feature-Instanzen".'
|
||||
: 'Wählen Sie eine Feature-Instanz aus, um deren Benutzer zu verwalten.'}
|
||||
? t('adminFeatureInstanceUsers.esGibtNochKeineFeatureinstanzen')
|
||||
: t('adminFeatureInstanceUsers.waehlenSieEineFeatureinstanzAus')}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -528,11 +532,11 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEditClick,
|
||||
title: 'Rollen bearbeiten',
|
||||
title: t('adminFeatureInstanceUsersPage.editRoles'),
|
||||
},
|
||||
{
|
||||
type: 'delete' as const,
|
||||
title: 'Aus Instanz entfernen',
|
||||
title: t('adminFeatureInstanceUsersPage.removeFromInstance'),
|
||||
}
|
||||
]}
|
||||
onDelete={handleRemoveUser}
|
||||
|
|
@ -549,7 +553,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
return false;
|
||||
},
|
||||
}}
|
||||
emptyMessage="Keine Benutzer gefunden"
|
||||
emptyMessage={t('adminFeatureInstanceUsers.keineBenutzerGefunden')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -559,7 +563,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
<div className={styles.modalOverlay} onClick={() => setShowAddModal(false)}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Benutzer zur Feature-Instanz hinzufügen</h2>
|
||||
<h2 className={styles.modalTitle}>{t('adminFeatureInstanceUsers.benutzerZurFeatureinstanzHinzufuegen')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setShowAddModal(false)}
|
||||
|
|
@ -569,17 +573,17 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
{availableUsers.length === 0 ? (
|
||||
<p>Alle Benutzer haben bereits Zugriff auf diese Feature-Instanz.</p>
|
||||
<p>{t('adminFeatureInstanceUsers.alleBenutzerHabenBereitsZugriff')}</p>
|
||||
) : instanceRoles.length === 0 ? (
|
||||
<p>Diese Feature-Instanz hat keine Rollen. Bitte synchronisieren Sie zuerst die Rollen.</p>
|
||||
<p>{t('adminFeatureInstanceUsers.dieseFeatureinstanzHatKeineRollen')}</p>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
attributes={addUserFields}
|
||||
mode="create"
|
||||
onSubmit={handleAddUser}
|
||||
onCancel={() => setShowAddModal(false)}
|
||||
submitButtonText="Hinzufügen"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('adminFeatureInstanceUsers.hinzufuegen')}
|
||||
cancelButtonText={t('adminFeatureInstanceUsers.abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -607,8 +611,8 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
|
|||
mode="edit"
|
||||
onSubmit={handleEditRoles}
|
||||
onCancel={() => setEditingUser(null)}
|
||||
submitButtonText="Speichern"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('adminFeatureInstanceUsers.speichern')}
|
||||
cancelButtonText={t('adminFeatureInstanceUsers.abbrechen')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import { useToast } from '../../contexts/ToastContext';
|
|||
import api from '../../api';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
interface Feature {
|
||||
id?: string;
|
||||
code: string; // Backend uses 'code' not 'featureCode'
|
||||
|
|
@ -40,6 +42,8 @@ interface FeatureRole {
|
|||
}
|
||||
|
||||
export const AdminFeatureRolesPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const { showError } = useToast();
|
||||
// State
|
||||
const [features, setFeatures] = useState<Feature[]>([]);
|
||||
|
|
@ -66,7 +70,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error loading features:', err);
|
||||
setError('Fehler beim Laden der Features');
|
||||
setError(t('adminFeatureRoles.fehlerBeimLadenDerFeatures'));
|
||||
}
|
||||
};
|
||||
loadFeatures();
|
||||
|
|
@ -108,7 +112,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error loading feature roles:', err);
|
||||
setError('Fehler beim Laden der Feature-Rollen');
|
||||
setError(t('adminFeatureRoles.fehlerBeimLadenDerFeaturerollen'));
|
||||
setRoles([]);
|
||||
setPagination(null);
|
||||
} finally {
|
||||
|
|
@ -131,7 +135,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
const columns = useMemo(() => [
|
||||
{
|
||||
key: 'roleLabel',
|
||||
label: 'Rollen-Label',
|
||||
label: t('adminFeatureRoles.rollenLabel'),
|
||||
type: 'string' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -140,7 +144,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: 'Beschreibung',
|
||||
label: t('adminFeatureRoles.beschreibung'),
|
||||
type: 'string' as const,
|
||||
sortable: false,
|
||||
width: 300,
|
||||
|
|
@ -148,7 +152,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'featureCode',
|
||||
label: 'Feature',
|
||||
label: t('adminFeatureRoles.feature'),
|
||||
type: 'string' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -159,49 +163,49 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
</span>
|
||||
)
|
||||
},
|
||||
], []);
|
||||
], [t]);
|
||||
|
||||
// Form attributes for create
|
||||
const createFields: AttributeDefinition[] = useMemo(() => {
|
||||
const fields: AttributeDefinition[] = [
|
||||
{
|
||||
name: 'roleLabel',
|
||||
label: 'Rollen-Label',
|
||||
label: t('adminFeatureRolesPage.rollenLabel'),
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Eindeutiger Bezeichner der Rolle (z.B. trustee-admin)'
|
||||
description: t('adminFeatureRolesPage.rollenLabelBeschreibung')
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Beschreibung',
|
||||
label: t('adminFeatureRolesPage.beschreibung'),
|
||||
type: 'multilingual',
|
||||
required: false,
|
||||
description: 'Mehrsprachige Beschreibung der Rolle'
|
||||
description: t('adminFeatureRolesPage.mehrsprachigeBeschreibung')
|
||||
}
|
||||
];
|
||||
return fields;
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
// Form attributes for edit
|
||||
const editFields: AttributeDefinition[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'roleLabel',
|
||||
label: 'Rollen-Label',
|
||||
label: t('adminFeatureRolesPage.rollenLabel'),
|
||||
type: 'string',
|
||||
required: true,
|
||||
readonly: true, // Label should not be changed after creation
|
||||
description: 'Eindeutiger Bezeichner der Rolle'
|
||||
readonly: true,
|
||||
description: t('adminFeatureRolesPage.rollenLabelReadonly')
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Beschreibung',
|
||||
label: t('adminFeatureRolesPage.beschreibung'),
|
||||
type: 'multilingual',
|
||||
required: false,
|
||||
description: 'Mehrsprachige Beschreibung der Rolle'
|
||||
description: t('adminFeatureRolesPage.mehrsprachigeBeschreibung')
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
// Handle create role
|
||||
const handleCreateRole = async (data: { roleLabel: string; description?: { [key: string]: string } }) => {
|
||||
|
|
@ -280,8 +284,8 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Feature Rollen & Rechte</h1>
|
||||
<p className={styles.pageSubtitle}>Template-Rollen und deren Berechtigungen für Feature-Instanzen verwalten</p>
|
||||
<h1 className={styles.pageTitle}>{t('adminFeatureRoles.featureRollenRechte')}</h1>
|
||||
<p className={styles.pageSubtitle}>{t('adminFeatureRoles.templaterollenUndDerenBerechtigungenFuer')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -297,7 +301,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
value={selectedFeatureCode}
|
||||
onChange={(e) => setSelectedFeatureCode(e.target.value)}
|
||||
>
|
||||
<option value="">-- Feature wählen --</option>
|
||||
<option value="">{t('adminFeatureRoles.featureWaehlen')}</option>
|
||||
{features.map(f => {
|
||||
const featureCode = f.code || f.featureCode || '';
|
||||
return (
|
||||
|
|
@ -343,7 +347,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
{!selectedFeatureCode ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaCube className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Kein Feature ausgewählt</h3>
|
||||
<h3 className={styles.emptyTitle}>{t('adminFeatureRoles.keinFeatureAusgewaehlt')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Wählen Sie ein Feature aus, um dessen Template-Rollen zu verwalten.
|
||||
</p>
|
||||
|
|
@ -365,11 +369,11 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEditClick,
|
||||
title: 'Rolle bearbeiten',
|
||||
title: t('adminFeatureRolesPage.editRole'),
|
||||
},
|
||||
{
|
||||
type: 'delete' as const,
|
||||
title: 'Rolle löschen',
|
||||
title: t('adminFeatureRolesPage.deleteRole'),
|
||||
}
|
||||
]}
|
||||
customActions={[
|
||||
|
|
@ -377,7 +381,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
id: 'permissions',
|
||||
icon: <FaShieldAlt />,
|
||||
onClick: (role: FeatureRole) => setPermissionsRole(role),
|
||||
title: 'Berechtigungen verwalten',
|
||||
title: t('adminFeatureRolesPage.managePermissions'),
|
||||
}
|
||||
]}
|
||||
onDelete={handleDeleteRole}
|
||||
|
|
@ -386,7 +390,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
pagination,
|
||||
handleDelete: handleDeleteRole,
|
||||
}}
|
||||
emptyMessage="Keine Feature-Rollen gefunden"
|
||||
emptyMessage={t('adminFeatureRoles.keineFeaturerollenGefunden')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -396,7 +400,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Neue Feature-Rolle erstellen</h2>
|
||||
<h2 className={styles.modalTitle}>{t('adminFeatureRoles.neueFeaturerolleErstellen')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
|
|
@ -415,7 +419,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
onSubmit={handleCreateRole}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
submitButtonText={isSubmitting ? 'Erstelle...' : 'Rolle erstellen'}
|
||||
cancelButtonText="Abbrechen"
|
||||
cancelButtonText={t('adminFeatureRoles.abbrechen')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -427,7 +431,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
<div className={styles.modalOverlay} onClick={() => setEditingRole(null)}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Feature-Rolle bearbeiten</h2>
|
||||
<h2 className={styles.modalTitle}>{t('adminFeatureRoles.featurerolleBearbeiten')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setEditingRole(null)}
|
||||
|
|
@ -446,8 +450,8 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
mode="edit"
|
||||
onSubmit={handleEditRole}
|
||||
onCancel={() => setEditingRole(null)}
|
||||
submitButtonText={isSubmitting ? 'Speichern...' : 'Speichern'}
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={isSubmitting ? t('adminFeatureRoles.speichern') : t('adminFeatureRoles.speichern')}
|
||||
cancelButtonText={t('adminFeatureRoles.abbrechen')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -474,7 +478,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
|
|||
<div className={styles.infoBox} style={{ marginBottom: '1rem' }}>
|
||||
<FaCube style={{ marginRight: 8 }} />
|
||||
<span>Feature: <strong>{permissionsRole.featureCode}</strong></span>
|
||||
<span style={{ marginLeft: '1rem' }}>Template-Rolle (global)</span>
|
||||
<span style={{ marginLeft: '1rem' }}>{t('adminFeatureRoles.templaterolleGlobal')}</span>
|
||||
</div>
|
||||
<AccessRulesEditor
|
||||
roleId={permissionsRole.id}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,16 @@ import { useInvitations, type Invitation, type InvitationCreate } from '../../ho
|
|||
import { useUserMandates, type Mandate, type Role } from '../../hooks/useUserMandates';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaPlus, FaSync, FaEnvelopeOpenText, FaBuilding, FaCopy, FaLink } from 'react-icons/fa';
|
||||
import { FaPlus, FaSync, FaBuilding, FaCopy, FaLink } from 'react-icons/fa';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import api from '../../api';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
export const AdminInvitationsPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const { showError } = useToast();
|
||||
const {
|
||||
invitations,
|
||||
|
|
@ -83,7 +87,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
const columns = useMemo(() => [
|
||||
{
|
||||
key: 'targetUsername',
|
||||
label: 'Benutzername',
|
||||
label: t('adminInvitations.benutzername'),
|
||||
type: 'string' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -92,7 +96,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: 'E-Mail',
|
||||
label: t('adminInvitations.email'),
|
||||
type: 'string' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -101,7 +105,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
const emailText = value || '-';
|
||||
const emailSent = (row as any).emailSent;
|
||||
return (
|
||||
<span title={emailSent ? 'Email wurde gesendet' : 'Email nicht gesendet'}>
|
||||
<span title={emailSent ? t('adminInvitations.emailWurdeGesendet') : t('adminInvitations.emailNichtGesendet')}>
|
||||
{emailText} {emailSent && '✓'}
|
||||
</span>
|
||||
);
|
||||
|
|
@ -109,7 +113,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'roleIds',
|
||||
label: 'Rollen',
|
||||
label: t('adminInvitations.rollen'),
|
||||
type: 'string', // Array rendered as string
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
|
|
@ -124,7 +128,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
} as any,
|
||||
{
|
||||
key: 'expiresAt',
|
||||
label: 'Gültig bis',
|
||||
label: t('adminInvitations.gueltigBis'),
|
||||
type: 'number' as const,
|
||||
sortable: true,
|
||||
width: 150,
|
||||
|
|
@ -140,7 +144,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'currentUses',
|
||||
label: 'Verwendet',
|
||||
label: t('adminInvitations.verwendet'),
|
||||
type: 'string' as const,
|
||||
sortable: true,
|
||||
width: 100,
|
||||
|
|
@ -148,13 +152,13 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
label: 'Erstellt',
|
||||
label: t('adminInvitations.erstellt'),
|
||||
type: 'number' as const,
|
||||
sortable: true,
|
||||
width: 150,
|
||||
render: (value: number) => formatDate(value)
|
||||
},
|
||||
], [roles]);
|
||||
], [roles, t]);
|
||||
|
||||
// Form attributes - same role options as AdminUserMandatesPage (user, viewer, admin)
|
||||
const createFields: AttributeDefinition[] = useMemo(() => {
|
||||
|
|
@ -174,7 +178,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
|
||||
// Add helper field expiresInHours if not in model but fields exist
|
||||
if (fields.length > 0 && !fields.find(f => f.name === 'expiresInHours')) {
|
||||
fields.push({ name: 'expiresInHours', label: 'Gültigkeitsdauer (Stunden)', type: 'number',
|
||||
fields.push({ name: 'expiresInHours', label: t('adminInvitationsPage.gueltigkeitsdauerStunden'), type: 'number',
|
||||
required: true, default: 72 } as any);
|
||||
}
|
||||
// Override required for targetUsername and email (both required for invitations)
|
||||
|
|
@ -258,8 +262,8 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Einladungen</h1>
|
||||
<p className={styles.pageSubtitle}>Erstellen und verwalten Sie Einladungen für neue Benutzer</p>
|
||||
<h1 className={styles.pageTitle}>{t('adminInvitations.einladungen')}</h1>
|
||||
<p className={styles.pageSubtitle}>{t('adminInvitations.erstellenUndVerwaltenSieEinladungen')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -275,7 +279,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
value={selectedMandateId}
|
||||
onChange={(e) => setSelectedMandateId(e.target.value)}
|
||||
>
|
||||
<option value="">-- Mandant wählen --</option>
|
||||
<option value="">{t('adminInvitations.mandantWaehlen')}</option>
|
||||
{mandates.map(m => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{getMandateName(m)}
|
||||
|
|
@ -326,7 +330,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
{!selectedMandateId ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Kein Mandant ausgewählt</h3>
|
||||
<h3 className={styles.emptyTitle}>{t('adminInvitations.keinMandantAusgewaehlt')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Wählen Sie einen Mandanten aus, um dessen Einladungen zu verwalten.
|
||||
</p>
|
||||
|
|
@ -347,7 +351,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
actionButtons={[
|
||||
{
|
||||
type: 'delete' as const,
|
||||
title: 'Einladung widerrufen',
|
||||
title: t('adminInvitationsPage.revokeInvitation'),
|
||||
}
|
||||
]}
|
||||
customActions={[
|
||||
|
|
@ -355,7 +359,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
id: 'showUrl',
|
||||
icon: <FaLink />,
|
||||
onClick: handleShowUrl,
|
||||
title: 'Einladungs-Link anzeigen',
|
||||
title: t('adminInvitationsPage.showInvitationLink'),
|
||||
}
|
||||
]}
|
||||
hookData={{
|
||||
|
|
@ -363,7 +367,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
refetch: (params?: any) => fetchInvitations(params || selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed }),
|
||||
pagination,
|
||||
}}
|
||||
emptyMessage="Keine Einladungen gefunden"
|
||||
emptyMessage={t('adminInvitations.keineEinladungenGefunden')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -373,7 +377,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Neue Einladung erstellen</h2>
|
||||
<h2 className={styles.modalTitle}>{t('adminInvitations.neueEinladungErstellen')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
|
|
@ -385,12 +389,12 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
{roles.filter(r => !r.featureInstanceId).length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Rollen...</span>
|
||||
<span>{t('adminInvitations.ladeRollen')}</span>
|
||||
</div>
|
||||
) : createFields.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Formular...</span>
|
||||
<span>{t('adminInvitations.ladeFormular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
|
|
@ -398,8 +402,8 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
mode="create"
|
||||
onSubmit={handleCreateInvitation}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
submitButtonText="Einladung erstellen"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('adminInvitations.einladungErstellen')}
|
||||
cancelButtonText={t('adminInvitations.abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -435,7 +439,7 @@ export const AdminInvitationsPage: React.FC = () => {
|
|||
<button
|
||||
className={styles.copyButton}
|
||||
onClick={() => handleCopyUrl(showUrlModal.inviteUrl)}
|
||||
title="In Zwischenablage kopieren"
|
||||
title={t('adminInvitations.inZwischenablageKopieren')}
|
||||
>
|
||||
<FaCopy />
|
||||
{copySuccess ? ' Kopiert!' : ' Kopieren'}
|
||||
|
|
|
|||
|
|
@ -23,14 +23,24 @@ type ProgressInfo = {
|
|||
total: number;
|
||||
error?: string;
|
||||
done?: boolean;
|
||||
/** Keys in language set before sync (from sync-diff). */
|
||||
keysCurrent?: number;
|
||||
/** New keys vs xx before PUT (from sync-diff). */
|
||||
keysPending?: number;
|
||||
/** Master (xx) key count (from sync-diff). */
|
||||
keysMasterTotal?: number;
|
||||
/** Keys AI-translated in last PUT (optional). */
|
||||
keysTranslated?: number;
|
||||
};
|
||||
|
||||
const _columns: ColumnConfig[] = [
|
||||
{ key: 'id', label: 'Code', type: 'text', sortable: true, filterable: true, width: 90 },
|
||||
{ key: 'label', label: 'Bezeichnung', type: 'text', sortable: true, filterable: true, width: 200 },
|
||||
{ key: 'status', label: 'Status', type: 'text', sortable: true, filterable: true, width: 120 },
|
||||
{ key: 'entriesCount', label: 'Einträge', type: 'number', sortable: true, width: 100 },
|
||||
function _getColumns(t: (key: string) => string): ColumnConfig[] {
|
||||
return [
|
||||
{ key: 'id', label: t('adminLanguages.code'), type: 'text', sortable: true, filterable: true, width: 90 },
|
||||
{ key: 'label', label: t('adminLanguages.bezeichnung'), type: 'text', sortable: true, filterable: true, width: 200 },
|
||||
{ key: 'status', label: t('adminLanguages.status'), type: 'text', sortable: true, filterable: true, width: 120 },
|
||||
{ key: 'entriesCount', label: t('adminLanguages.eintraege'), type: 'number', sortable: true, width: 100 },
|
||||
];
|
||||
}
|
||||
|
||||
const _PRIORITY_CODES = ['de', 'en', 'fr', 'it'];
|
||||
|
||||
|
|
@ -86,7 +96,28 @@ const _isoChoices: { value: string; label: string }[] = [
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
const _ProgressOverlay: React.FC<{ progress: ProgressInfo }> = ({ progress }) => {
|
||||
const { t } = useLanguage();
|
||||
const pct = progress.total > 0 ? Math.round((progress.current / progress.total) * 100) : 0;
|
||||
const master = progress.keysMasterTotal;
|
||||
let keysLine: string | null = null;
|
||||
if (master != null && master > 0) {
|
||||
const pending = progress.keysPending ?? 0;
|
||||
if (pending > 0 && progress.keysTranslated !== undefined) {
|
||||
keysLine = t('{tr} / {pending} neue Schlüssel übersetzt · Basis {m}', {
|
||||
tr: String(progress.keysTranslated),
|
||||
pending: String(pending),
|
||||
m: String(master),
|
||||
});
|
||||
} else if (pending > 0) {
|
||||
keysLine = t('{pending} / {m} (neu / Basis-Schlüssel)', {
|
||||
pending: String(pending),
|
||||
m: String(master),
|
||||
});
|
||||
} else {
|
||||
const cur = progress.keysCurrent ?? 0;
|
||||
keysLine = t('{cur} / {m} Schlüssel abgedeckt', { cur: String(cur), m: String(master) });
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
|
@ -138,10 +169,18 @@ const _ProgressOverlay: React.FC<{ progress: ProgressInfo }> = ({ progress }) =>
|
|||
}}
|
||||
/>
|
||||
</div>
|
||||
{progress.total > 1 && (
|
||||
<p style={{ fontSize: '0.85rem', opacity: 0.7 }}>
|
||||
{progress.current} / {progress.total}
|
||||
{progress.done && !progress.error && ' — fertig'}
|
||||
{t('Schritt {cur} von {tot}', { cur: String(progress.current), tot: String(progress.total) })}
|
||||
{progress.done && !progress.error && ` — ${t('fertig')}`}
|
||||
</p>
|
||||
)}
|
||||
{progress.total === 1 && progress.done && !progress.error && (
|
||||
<p style={{ fontSize: '0.85rem', opacity: 0.7 }}>{t('fertig')}</p>
|
||||
)}
|
||||
{keysLine && (
|
||||
<p style={{ fontSize: '0.8rem', opacity: 0.75, marginTop: '0.35rem' }}>{keysLine}</p>
|
||||
)}
|
||||
{progress.error && (
|
||||
<p style={{ color: 'var(--error-color, #c53030)', fontSize: '0.85rem', marginTop: '0.5rem' }}>
|
||||
{progress.error}
|
||||
|
|
@ -269,10 +308,39 @@ export const AdminLanguagesPage: React.FC = () => {
|
|||
busyRef.current = true;
|
||||
setError(null);
|
||||
const label = rows.find((r) => r.id === code)?.label || code;
|
||||
setProgress({ message: t('Aktualisiere {lang}…', { lang: label }), current: 0, total: 1 });
|
||||
let keysCurrent: number | undefined;
|
||||
let keysPending: number | undefined;
|
||||
let keysMasterTotal: number | undefined;
|
||||
try {
|
||||
await api.put(`/api/i18n/sets/${encodeURIComponent(code)}`);
|
||||
setProgress({ message: t('{lang} aktualisiert.', { lang: label }), current: 1, total: 1, done: true });
|
||||
const dr = await api.get(`/api/i18n/sets/${encodeURIComponent(code)}/sync-diff`);
|
||||
keysCurrent = dr.data?.currentEntryCount;
|
||||
keysPending = dr.data?.addedCount;
|
||||
keysMasterTotal = dr.data?.masterEntryCount;
|
||||
} catch {
|
||||
/* sync-diff optional */
|
||||
}
|
||||
setProgress({
|
||||
message: t('Aktualisiere {lang}…', { lang: label }),
|
||||
current: 0,
|
||||
total: 1,
|
||||
keysCurrent,
|
||||
keysPending,
|
||||
keysMasterTotal,
|
||||
});
|
||||
try {
|
||||
const putRes = await api.put(`/api/i18n/sets/${encodeURIComponent(code)}`);
|
||||
const d = putRes.data || {};
|
||||
const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0);
|
||||
setProgress({
|
||||
message: t('{lang} aktualisiert.', { lang: label }),
|
||||
current: 1,
|
||||
total: 1,
|
||||
done: true,
|
||||
keysCurrent: typeof d.entriesCount === 'number' ? d.entriesCount : keysCurrent,
|
||||
keysPending: pendingAfterPut,
|
||||
keysMasterTotal,
|
||||
keysTranslated: typeof d.translated === 'number' ? d.translated : undefined,
|
||||
});
|
||||
await _load();
|
||||
await refreshAvailableLanguages();
|
||||
await reloadLanguage();
|
||||
|
|
@ -318,13 +386,38 @@ export const AdminLanguagesPage: React.FC = () => {
|
|||
const errors: string[] = [];
|
||||
for (const code of langCodes) {
|
||||
const label = rows.find((r) => r.id === code)?.label || code;
|
||||
let keysCurrent: number | undefined;
|
||||
let keysPending: number | undefined;
|
||||
let keysMasterTotal: number | undefined;
|
||||
try {
|
||||
const dr = await api.get(`/api/i18n/sets/${encodeURIComponent(code)}/sync-diff`);
|
||||
keysCurrent = dr.data?.currentEntryCount;
|
||||
keysPending = dr.data?.addedCount;
|
||||
keysMasterTotal = dr.data?.masterEntryCount;
|
||||
} catch {
|
||||
/* sync-diff optional */
|
||||
}
|
||||
setProgress({
|
||||
message: t('Aktualisiere {lang}… ({n}/{total})', { lang: label, n: String(step + 1), total: String(totalSteps) }),
|
||||
current: step,
|
||||
message: t('Aktualisiere {lang}…', { lang: label }),
|
||||
current: step + 1,
|
||||
total: totalSteps,
|
||||
keysCurrent,
|
||||
keysPending,
|
||||
keysMasterTotal,
|
||||
});
|
||||
try {
|
||||
await api.put(`/api/i18n/sets/${encodeURIComponent(code)}`);
|
||||
const putRes = await api.put(`/api/i18n/sets/${encodeURIComponent(code)}`);
|
||||
const d = putRes.data || {};
|
||||
const pendingAfterPut = Array.isArray(d.added) ? d.added.length : (keysPending ?? 0);
|
||||
setProgress({
|
||||
message: t('Aktualisiere {lang}…', { lang: label }),
|
||||
current: step + 1,
|
||||
total: totalSteps,
|
||||
keysCurrent: typeof d.entriesCount === 'number' ? d.entriesCount : keysCurrent,
|
||||
keysPending: pendingAfterPut,
|
||||
keysMasterTotal,
|
||||
keysTranslated: typeof d.translated === 'number' ? d.translated : undefined,
|
||||
});
|
||||
} catch (e: any) {
|
||||
errors.push(`${code}: ${e.response?.data?.detail || e.message}`);
|
||||
}
|
||||
|
|
@ -530,7 +623,7 @@ export const AdminLanguagesPage: React.FC = () => {
|
|||
<div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
|
||||
<FormGeneratorTable
|
||||
data={rows}
|
||||
columns={_columns}
|
||||
columns={_getColumns(t)}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
selectable={false}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import api from '../../api';
|
|||
import styles from './Admin.module.css';
|
||||
import logStyles from './AdminLogsPage.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
const LOG_LEVEL_COLORS: Record<string, string> = {
|
||||
ERROR: 'var(--log-error, #e53e3e)',
|
||||
WARNING: 'var(--log-warning, #d69e2e)',
|
||||
|
|
@ -27,6 +29,8 @@ function _parseLogLevel(line: string): string | null {
|
|||
}
|
||||
|
||||
export const AdminLogsPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const [lines, setLines] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -104,7 +108,7 @@ export const AdminLogsPage: React.FC = () => {
|
|||
<div className={styles.adminPage}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Gateway Logs</h1>
|
||||
<h1 className={styles.pageTitle}>{t('adminLogs.gatewayLogs')}</h1>
|
||||
<p className={styles.pageSubtitle}>
|
||||
{lines.length > 0
|
||||
? `${lines.length} Einträge`
|
||||
|
|
@ -117,7 +121,7 @@ export const AdminLogsPage: React.FC = () => {
|
|||
className={styles.secondaryButton}
|
||||
onClick={_handleDownload}
|
||||
disabled={lines.length === 0}
|
||||
title="Log herunterladen"
|
||||
title={t('adminLogs.logHerunterladen')}
|
||||
>
|
||||
<FaDownload /> Download
|
||||
</button>
|
||||
|
|
@ -136,7 +140,7 @@ export const AdminLogsPage: React.FC = () => {
|
|||
min={1}
|
||||
max={50000}
|
||||
/>
|
||||
<label className={logStyles.controlLabel}>Einträge</label>
|
||||
<label className={logStyles.controlLabel}>{t('adminLogs.eintraege')}</label>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={_handleLoad}
|
||||
|
|
@ -179,7 +183,7 @@ export const AdminLogsPage: React.FC = () => {
|
|||
{lines.length === 0 && !loading && (
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyIcon}>📋</div>
|
||||
<p className={styles.emptyTitle}>Keine Logs geladen</p>
|
||||
<p className={styles.emptyTitle}>{t('adminLogs.keineLogsGeladen')}</p>
|
||||
<p className={styles.emptyDescription}>
|
||||
Gib die gewünschte Anzahl Einträge ein und klicke auf "Laden".
|
||||
</p>
|
||||
|
|
@ -188,7 +192,7 @@ export const AdminLogsPage: React.FC = () => {
|
|||
{loading && lines.length === 0 && (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Logs werden geladen...</span>
|
||||
<span>{t('adminLogs.logsWerdenGeladen')}</span>
|
||||
</div>
|
||||
)}
|
||||
{lines.map((line, idx) => {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ import {
|
|||
} from 'react-icons/fa';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
// Types for cleanup result
|
||||
interface DuplicateGroup {
|
||||
roleId: string;
|
||||
|
|
@ -73,6 +75,8 @@ interface TemplateFixResult {
|
|||
}
|
||||
|
||||
export const AdminMandateRolePermissionsPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const {
|
||||
roles,
|
||||
loading,
|
||||
|
|
@ -212,10 +216,10 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
|
||||
// Filter options for scope
|
||||
const scopeOptions = useMemo(() => [
|
||||
{ value: 'mandate', label: 'Mandanten-Rollen' },
|
||||
{ value: 'all', label: 'Alle (inkl. Templates)' },
|
||||
{ value: 'global', label: 'Nur Templates' },
|
||||
], []);
|
||||
{ value: 'mandate', label: t('adminMandateRolePermissionsPage.mandantenRollen') },
|
||||
{ value: 'all', label: t('adminMandateRolePermissionsPage.alleInklTemplates') },
|
||||
{ value: 'global', label: t('adminMandateRolePermissionsPage.nurTemplates') },
|
||||
], [t]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
|
@ -249,7 +253,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
className={styles.secondaryButton}
|
||||
onClick={_openCleanupModal}
|
||||
disabled={loading}
|
||||
title="Doppelte Regeln finden und bereinigen"
|
||||
title={t('adminMandateRolePermissions.doppelteRegelnFindenUndBereinigen')}
|
||||
>
|
||||
<FaBroom /> Duplikate bereinigen
|
||||
</button>
|
||||
|
|
@ -266,7 +270,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
{/* Filters */}
|
||||
<div className={styles.filterBar}>
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>Mandant:</label>
|
||||
<label className={styles.filterLabel}>{t('adminMandateRolePermissions.mandant')}</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
value={selectedMandateId}
|
||||
|
|
@ -310,7 +314,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
{loading && (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Rollen...</span>
|
||||
<span>{t('adminMandateRolePermissions.ladeRollen')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -318,13 +322,13 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
{!loading && roles.length === 0 && (
|
||||
<div className={styles.emptyState}>
|
||||
<FaUserShield className={styles.emptyIcon} />
|
||||
<p>Keine Rollen gefunden</p>
|
||||
<p>{t('adminMandateRolePermissions.keineRollenGefunden')}</p>
|
||||
<p className={styles.emptyHint}>
|
||||
{scopeFilter === 'mandate'
|
||||
? 'Es gibt noch keine Mandanten-Rollen. System-Rollen werden bei der Mandant-Erstellung automatisch kopiert.'
|
||||
: scopeFilter === 'global'
|
||||
? 'Es gibt noch keine Rollen-Templates.'
|
||||
: 'Es gibt noch keine Rollen für diesen Mandanten.'}
|
||||
? t('adminMandateRolePermissions.esGibtNochKeineRollentemplates')
|
||||
: t('adminMandateRolePermissions.esGibtNochKeineRollen')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -399,7 +403,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
{cleanupLoading && (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>{cleanupPhase === 'idle' ? 'Analysiere Duplikate...' : 'Bereinige Duplikate...'}</span>
|
||||
<span>{cleanupPhase === 'idle' ? t('adminMandateRolePermissions.analysiereDuplikate') : t('adminMandateRolePermissions.bereinigeDuplikate')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -418,11 +422,11 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '0.75rem', marginBottom: '1.25rem' }}>
|
||||
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{cleanupResult.totalRules}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Regeln total</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminMandateRolePermissions.regelnTotal')}</div>
|
||||
</div>
|
||||
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{cleanupResult.uniqueSignatures}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Eindeutige Regeln</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminMandateRolePermissions.eindeutigeRegeln')}</div>
|
||||
</div>
|
||||
<div style={{ padding: '0.75rem', background: cleanupResult.duplicateGroups > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${cleanupResult.duplicateGroups > 0 ? '#fc8181' : '#9ae6b4'}` }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: cleanupResult.duplicateGroups > 0 ? '#c53030' : '#2f855a' }}>{cleanupResult.duplicateGroups}</div>
|
||||
|
|
@ -442,14 +446,14 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
{cleanupPhase === 'done' && (
|
||||
<div style={{ padding: '0.75rem 1rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
|
||||
<FaCheckCircle />
|
||||
<span><strong>{cleanupResult.deletedCount}</strong> doppelte Regeln wurden erfolgreich entfernt.</span>
|
||||
<span><strong>{cleanupResult.deletedCount}</strong> {t('adminMandateRolePermissions.doppelteRegelnWurdenErfolgreichEntfernt')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{cleanupPhase === 'preview' && cleanupResult.duplicateGroups === 0 && (
|
||||
<div style={{ padding: '0.75rem 1rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
|
||||
<FaCheckCircle />
|
||||
<span>Keine Duplikate gefunden. Alles sauber!</span>
|
||||
<span>{t('adminMandateRolePermissions.keineDuplikateGefundenAllesSauber')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -501,11 +505,11 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '0.75rem', marginBottom: '1rem' }}>
|
||||
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)' }}>{templateFixResult.totalUserMandateRoles}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Rollen-Zuweisungen total</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminMandateRolePermissions.rollenzuweisungenTotal')}</div>
|
||||
</div>
|
||||
<div style={{ padding: '0.75rem', background: templateFixResult.invalidAssignments > 0 ? '#fff5f5' : '#f0fff4', borderRadius: '8px', textAlign: 'center', border: `1px solid ${templateFixResult.invalidAssignments > 0 ? '#fc8181' : '#9ae6b4'}` }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: templateFixResult.invalidAssignments > 0 ? '#c53030' : '#2f855a' }}>{templateFixResult.invalidAssignments}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>Template statt Instanz</div>
|
||||
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminMandateRolePermissions.templateStattInstanz')}</div>
|
||||
</div>
|
||||
<div style={{ padding: '0.75rem', background: 'var(--bg-secondary)', borderRadius: '8px', textAlign: 'center', border: '1px solid var(--border-color)' }}>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 700, color: templateFixResult.fixedCount > 0 ? '#2f855a' : 'var(--text-primary)' }}>
|
||||
|
|
@ -522,8 +526,8 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8125rem' }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'var(--bg-secondary)', position: 'sticky', top: 0 }}>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>Rolle</th>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>Mandant</th>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('adminMandateRolePermissions.rolle')}</th>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'left', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>{t('adminMandateRolePermissions.mandant')}</th>
|
||||
<th style={{ padding: '0.5rem 0.75rem', textAlign: 'center', fontWeight: 600, borderBottom: '1px solid var(--border-color)' }}>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -563,7 +567,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
{templateFixResult && templateFixResult.invalidAssignments === 0 && (
|
||||
<div style={{ marginTop: '1rem', padding: '0.5rem 0.75rem', background: '#f0fff4', borderRadius: '6px', color: '#2f855a', fontSize: '0.875rem', display: 'flex', alignItems: 'center', gap: '0.5rem', border: '1px solid #9ae6b4' }}>
|
||||
<FaCheckCircle />
|
||||
<span>Keine fehlerhaften Template-Rollen-Zuweisungen.</span>
|
||||
<span>{t('adminMandateRolePermissions.keineFehlerhaftenTemplaterollenzuweisungen')}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -572,7 +576,7 @@ export const AdminMandateRolePermissionsPage: React.FC = () => {
|
|||
|
||||
<div className={styles.modalFooter}>
|
||||
<button className={styles.secondaryButton} onClick={_closeCleanupModal}>
|
||||
{cleanupPhase === 'done' ? 'Schliessen' : 'Abbrechen'}
|
||||
{cleanupPhase === 'done' ? t('adminMandateRolePermissions.schliessen') : t('adminMandateRolePermissions.abbrechen')}
|
||||
</button>
|
||||
{cleanupPhase === 'preview' && cleanupResult && (cleanupResult.duplicateRulesToDelete > 0 || (templateFixResult && templateFixResult.invalidAssignments > 0)) && (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ import { useToast } from '../../contexts/ToastContext';
|
|||
import api from '../../api';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
export const AdminMandateRolesPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { showError, showWarning } = useToast();
|
||||
const {
|
||||
|
|
@ -99,7 +103,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
const columns = useMemo(() => [
|
||||
{
|
||||
key: 'roleLabel',
|
||||
label: 'Bezeichnung',
|
||||
label: t('adminMandateRoles.bezeichnung'),
|
||||
type: 'string' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -108,7 +112,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'description',
|
||||
label: 'Beschreibung',
|
||||
label: t('adminMandateRoles.beschreibung'),
|
||||
type: 'string' as const,
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
|
|
@ -117,7 +121,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'scopeType',
|
||||
label: 'Geltungsbereich',
|
||||
label: t('adminMandateRoles.geltungsbereich'),
|
||||
type: 'string' as const,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -144,7 +148,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
);
|
||||
}
|
||||
},
|
||||
], []);
|
||||
], [t]);
|
||||
|
||||
// Form attributes from backend - for create form
|
||||
const createFields: AttributeDefinition[] = useMemo(() => {
|
||||
|
|
@ -158,13 +162,13 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
if (fields.length > 0) {
|
||||
fields.push({
|
||||
name: 'scope',
|
||||
label: 'Geltungsbereich',
|
||||
label: t('adminMandateRolesPage.geltungsbereich'),
|
||||
type: 'enum' as any,
|
||||
required: true,
|
||||
default: 'mandate',
|
||||
options: [
|
||||
{ value: 'mandate', label: 'Nur dieser Mandant' },
|
||||
{ value: 'global', label: 'Template (wird bei neuen Mandanten kopiert)' },
|
||||
{ value: 'mandate', label: t('adminMandateRolesPage.nurDieserMandant') },
|
||||
{ value: 'global', label: t('adminMandateRolesPage.templateBeiNeuenMandanten') },
|
||||
]
|
||||
});
|
||||
}
|
||||
|
|
@ -310,8 +314,8 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Rollen</h1>
|
||||
<p className={styles.pageSubtitle}>Verwalten Sie System-, globale und mandantenspezifische Rollen</p>
|
||||
<h1 className={styles.pageTitle}>{t('adminMandateRoles.rollen')}</h1>
|
||||
<p className={styles.pageSubtitle}>{t('adminMandateRoles.verwaltenSieSystemGlobaleUnd')}</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
|
|
@ -343,7 +347,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
value={selectedMandateId}
|
||||
onChange={(e) => setSelectedMandateId(e.target.value)}
|
||||
>
|
||||
<option value="">-- Mandant wählen --</option>
|
||||
<option value="">{t('adminMandateRoles.mandantWaehlen')}</option>
|
||||
{mandates.map(m => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{getMandateName(m)}
|
||||
|
|
@ -353,7 +357,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div className={styles.filterGroup}>
|
||||
<label className={styles.filterLabel}>Filter:</label>
|
||||
<label className={styles.filterLabel}>{t('adminMandateRoles.filter')}</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
value={scopeFilter}
|
||||
|
|
@ -361,8 +365,8 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
style={{ minWidth: 150 }}
|
||||
>
|
||||
<option value="mandate">Mandanten-Rollen</option>
|
||||
<option value="all">Alle (inkl. Templates)</option>
|
||||
<option value="global">Nur Templates</option>
|
||||
<option value="all">{t('adminMandateRoles.alleInklTemplates')}</option>
|
||||
<option value="global">{t('adminMandateRoles.nurTemplates')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
@ -401,7 +405,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
{!selectedMandateId ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Kein Mandant ausgewählt</h3>
|
||||
<h3 className={styles.emptyTitle}>{t('adminMandateRoles.keinMandantAusgewaehlt')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Wählen Sie einen Mandanten aus, um dessen Rollen zu verwalten.
|
||||
</p>
|
||||
|
|
@ -423,12 +427,12 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEditClick,
|
||||
title: 'Rolle bearbeiten',
|
||||
title: t('adminMandateRolesPage.editRole'),
|
||||
disabled: (row: Role) => row.isSystemRole ? { disabled: true, message: 'System-Rollen können nicht bearbeitet werden' } : false
|
||||
},
|
||||
{
|
||||
type: 'delete' as const,
|
||||
title: 'Rolle löschen',
|
||||
title: t('adminMandateRolesPage.deleteRole'),
|
||||
disabled: (row: Role) => row.isSystemRole ? { disabled: true, message: 'System-Rollen können nicht gelöscht werden' } : false
|
||||
}
|
||||
]}
|
||||
|
|
@ -438,7 +442,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
pagination: pagination,
|
||||
handleDelete: handleDeleteRole,
|
||||
}}
|
||||
emptyMessage="Keine Rollen gefunden"
|
||||
emptyMessage={t('adminMandateRoles.keineRollenGefunden')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -448,7 +452,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
<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 erstellen</h2>
|
||||
<h2 className={styles.modalTitle}>{t('adminMandateRoles.neueRolleErstellen')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
|
|
@ -460,7 +464,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
{createFields.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Formular...</span>
|
||||
<span>{t('adminMandateRoles.ladeFormular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
|
|
@ -469,7 +473,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
onSubmit={handleCreateRole}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
submitButtonText={isSubmitting ? 'Erstelle...' : 'Rolle erstellen'}
|
||||
cancelButtonText="Abbrechen"
|
||||
cancelButtonText={t('adminMandateRoles.abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -494,7 +498,7 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
{editFields.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Formular...</span>
|
||||
<span>{t('adminMandateRoles.ladeFormular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -511,8 +515,8 @@ export const AdminMandateRolesPage: React.FC = () => {
|
|||
mode="edit"
|
||||
onSubmit={handleEditRole}
|
||||
onCancel={() => setEditingRole(null)}
|
||||
submitButtonText={isSubmitting ? 'Speichern...' : 'Speichern'}
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={isSubmitting ? t('adminMandateRoles.speichern') : t('adminMandateRoles.speichern')}
|
||||
cancelButtonText={t('adminMandateRoles.abbrechen')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,14 @@ import { useToast } from '../../contexts/ToastContext';
|
|||
import { usePrompt } from '../../hooks/usePrompt';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaPlus, FaSync, FaBuilding, FaUsers, FaLock, FaSkullCrossbones } from 'react-icons/fa';
|
||||
import { FaPlus, FaSync, FaUsers, FaLock, FaSkullCrossbones } from 'react-icons/fa';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
export const AdminMandatesPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { request } = useApiRequest();
|
||||
const { showWarning, showSuccess } = useToast();
|
||||
|
|
@ -120,7 +124,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
}
|
||||
const entered = await prompt(
|
||||
`Um den Mandanten "${mandate.name}" zu deaktivieren (Soft-Delete), geben Sie den Namen ein:`,
|
||||
{ title: 'Mandant deaktivieren', confirmLabel: 'Deaktivieren', variant: 'danger', placeholder: mandate.name },
|
||||
{ title: t('adminMandatesPage.deactivateMandate'), confirmLabel: t('adminMandatesPage.deactivate'), variant: 'danger', placeholder: mandate.name },
|
||||
);
|
||||
if (entered === null) return;
|
||||
if (entered !== mandate.name) {
|
||||
|
|
@ -137,7 +141,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
}
|
||||
const entered = await prompt(
|
||||
`ACHTUNG: Dies löscht den Mandanten "${mandate.name}" unwiderruflich inkl. aller Subscriptions, Features, Benutzer-Zuweisungen und Daten. Geben Sie den exakten Namen ein:`,
|
||||
{ title: 'Hard Delete (irreversibel)', confirmLabel: 'Endgültig löschen', variant: 'danger', placeholder: mandate.name },
|
||||
{ title: t('adminMandatesPage.hardDeleteIrreversible'), confirmLabel: t('adminMandatesPage.deletePermanently'), variant: 'danger', placeholder: mandate.name },
|
||||
);
|
||||
if (entered === null) return;
|
||||
if (entered !== mandate.name) {
|
||||
|
|
@ -169,7 +173,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Mandanten</h1>
|
||||
<p className={styles.pageSubtitle}>Verwalten Sie alle Mandanten im System</p>
|
||||
<p className={styles.pageSubtitle}>{t('adminMandates.verwaltenSieAlleMandantenIm')}</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
|
|
@ -213,11 +217,11 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
...(canUpdate ? [{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEditClick,
|
||||
title: 'Bearbeiten',
|
||||
title: t('adminMandatesPage.edit'),
|
||||
}] : []),
|
||||
...(canDelete ? [{
|
||||
type: 'delete' as const,
|
||||
title: 'Deaktivieren (Soft-Delete)',
|
||||
title: t('adminMandatesPage.deactivateSoftDelete'),
|
||||
disabled: (row: Mandate) => row.isSystem
|
||||
? { disabled: true, message: 'System-Mandanten können nicht gelöscht werden' }
|
||||
: false
|
||||
|
|
@ -227,7 +231,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
id: 'hard-delete',
|
||||
icon: <FaSkullCrossbones />,
|
||||
onClick: handleHardDeleteMandate,
|
||||
title: 'Hard Delete (irreversibel)',
|
||||
title: t('adminMandatesPage.hardDeleteIrreversible'),
|
||||
disabled: (row: Mandate) => row.isSystem
|
||||
? { disabled: true, message: 'System-Mandanten können nicht gelöscht werden' }
|
||||
: false,
|
||||
|
|
@ -241,7 +245,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
handleInlineUpdate,
|
||||
updateOptimistically,
|
||||
}}
|
||||
emptyMessage="Keine Mandanten gefunden"
|
||||
emptyMessage={t('adminMandates.keineMandantenGefunden')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -250,7 +254,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
<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>
|
||||
<h2 className={styles.modalTitle}>{t('adminMandates.neuerMandant')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
|
|
@ -266,7 +270,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
{mandateAttrsLoading || createFormAttributes.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Formular...</span>
|
||||
<span>{t('adminMandates.ladeFormular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
|
|
@ -274,8 +278,8 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
mode="create"
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
submitButtonText="Erstellen"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('adminMandates.erstellen')}
|
||||
cancelButtonText={t('adminMandates.abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -296,7 +300,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Mandant bearbeiten</h2>
|
||||
<h2 className={styles.modalTitle}>{t('adminMandates.mandantBearbeiten')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => {
|
||||
|
|
@ -327,7 +331,7 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
{formAttributes.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Formular...</span>
|
||||
<span>{t('adminMandates.ladeFormular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
|
|
@ -339,8 +343,8 @@ export const AdminMandatesPage: React.FC = () => {
|
|||
setEditingFormData(null);
|
||||
setEditingBillingWarning(null);
|
||||
}}
|
||||
submitButtonText="Speichern"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('adminMandates.speichern')}
|
||||
cancelButtonText={t('adminMandates.abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { FaSync, FaUserShield, FaEye, FaDatabase, FaCube, FaChevronDown, FaChevr
|
|||
import api from '../../api';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
interface UserOption {
|
||||
id: string;
|
||||
username: string;
|
||||
|
|
@ -84,6 +86,8 @@ interface UserAccessOverview {
|
|||
type TabId = 'overview' | 'ui' | 'data' | 'resources';
|
||||
|
||||
export const AdminUserAccessOverviewPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const [users, setUsers] = useState<UserOption[]>([]);
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>('');
|
||||
const [overview, setOverview] = useState<UserAccessOverview | null>(null);
|
||||
|
|
@ -201,10 +205,10 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<h3 style={{ marginBottom: '1rem', color: 'var(--text-primary)' }}>Zugriff nach Mandant</h3>
|
||||
<h3 style={{ marginBottom: '1rem', color: 'var(--text-primary)' }}>{t('adminUserAccessOverview.zugriffNachMandant')}</h3>
|
||||
|
||||
{overview.mandates.length === 0 ? (
|
||||
<p className={styles.emptyHint}>Keine Mandate-Zuordnungen vorhanden.</p>
|
||||
<p className={styles.emptyHint}>{t('adminUserAccessOverview.keineMandatezuordnungenVorhanden')}</p>
|
||||
) : (
|
||||
<div className={styles.rolesList} style={{ flex: 'none', overflow: 'visible' }}>
|
||||
{overview.mandates.map((mandate) => {
|
||||
|
|
@ -228,7 +232,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
{expandedMandates.has(mandate.id) && (
|
||||
<div className={styles.roleContent}>
|
||||
{mandateRoles.length === 0 ? (
|
||||
<p className={styles.emptyHint}>Keine Rollen direkt am Mandanten.</p>
|
||||
<p className={styles.emptyHint}>{t('adminUserAccessOverview.keineRollenDirektAmMandanten')}</p>
|
||||
) : (
|
||||
<ul className={styles.accessOverviewRoleBullets}>
|
||||
{mandateRoles.map((r) => (
|
||||
|
|
@ -253,7 +257,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
|
||||
<div className={styles.accessOverviewSubheading}>Feature-Instanzen</div>
|
||||
{mandate.featureInstances.length === 0 ? (
|
||||
<p className={styles.emptyHint}>Keine Feature-Instanzen zugewiesen.</p>
|
||||
<p className={styles.emptyHint}>{t('adminUserAccessOverview.keineFeatureinstanzenZugewiesen')}</p>
|
||||
) : (
|
||||
<div className={styles.accessOverviewInstanceStack}>
|
||||
{mandate.featureInstances.map((instance) => {
|
||||
|
|
@ -334,7 +338,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
<div className={styles.roleContent}>
|
||||
<div style={{ fontSize: '0.875rem' }}>
|
||||
<p>
|
||||
<strong>Beschreibung:</strong> {_roleDescriptionLine(role) || '—'}
|
||||
<strong>{t('adminUserAccessOverview.beschreibung')}</strong> {_roleDescriptionLine(role) || '—'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -355,13 +359,13 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
<div className={styles.scrollableContent}>
|
||||
<div className={styles.infoBox}>
|
||||
<FaInfoCircle style={{ marginRight: '0.5rem', color: 'var(--primary-color)' }} />
|
||||
<span>UI-Zugriffsrechte bestimmen, welche Seiten und Views der Benutzer sehen kann.</span>
|
||||
<span>{t('adminUserAccessOverview.uizugriffsrechteBestimmenWelcheSeitenUnd')}</span>
|
||||
</div>
|
||||
|
||||
{overview.uiAccess.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaEye className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Keine UI-Berechtigungen</h3>
|
||||
<h3 className={styles.emptyTitle}>{t('adminUserAccessOverview.keineUiberechtigungen')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Diesem Benutzer wurden keine expliziten UI-Berechtigungen zugewiesen.
|
||||
</p>
|
||||
|
|
@ -372,7 +376,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
<tr style={{ borderBottom: '2px solid var(--border-color)' }}>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>UI-Element</th>
|
||||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '80px' }}>Sichtbar</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Gewährt durch</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminUserAccessOverview.gewaehrtDurch')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -422,7 +426,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
{overview.dataAccess.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaDatabase className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Keine Daten-Berechtigungen</h3>
|
||||
<h3 className={styles.emptyTitle}>{t('adminUserAccessOverview.keineDatenberechtigungen')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Diesem Benutzer wurden keine expliziten Daten-Berechtigungen zugewiesen.
|
||||
</p>
|
||||
|
|
@ -433,10 +437,10 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
<tr style={{ borderBottom: '2px solid var(--border-color)' }}>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Tabelle/Feld</th>
|
||||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>Lesen</th>
|
||||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>Erstellen</th>
|
||||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>{t('adminUserAccessOverview.erstellen')}</th>
|
||||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>Update</th>
|
||||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>Löschen</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Gewährt durch</th>
|
||||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '70px' }}>{t('adminUserAccessOverview.loeschen')}</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminUserAccessOverview.gewaehrtDurch')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -518,13 +522,13 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
<div className={styles.scrollableContent}>
|
||||
<div className={styles.infoBox}>
|
||||
<FaInfoCircle style={{ marginRight: '0.5rem', color: 'var(--primary-color)' }} />
|
||||
<span>Ressourcen-Zugriffsrechte bestimmen, welche System-Ressourcen (z.B. AI-Modelle) der Benutzer verwenden kann.</span>
|
||||
<span>{t('adminUserAccessOverview.ressourcenzugriffsrechteBestimmenWelcheSystemressourcenZb')}</span>
|
||||
</div>
|
||||
|
||||
{overview.resourceAccess.length === 0 ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaCube className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Keine Ressourcen-Berechtigungen</h3>
|
||||
<h3 className={styles.emptyTitle}>{t('adminUserAccessOverview.keineRessourcenberechtigungen')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Diesem Benutzer wurden keine expliziten Ressourcen-Berechtigungen zugewiesen.
|
||||
</p>
|
||||
|
|
@ -535,7 +539,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
<tr style={{ borderBottom: '2px solid var(--border-color)' }}>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Ressource</th>
|
||||
<th style={{ textAlign: 'center', padding: '0.75rem', color: 'var(--text-secondary)', width: '80px' }}>Zugriff</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>Gewährt durch</th>
|
||||
<th style={{ textAlign: 'left', padding: '0.75rem', color: 'var(--text-secondary)' }}>{t('adminUserAccessOverview.gewaehrtDurch')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -590,8 +594,8 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Benutzer-Zugriffsübersicht</h1>
|
||||
<p className={styles.pageSubtitle}>Zeigt alle Berechtigungen eines Benutzers an</p>
|
||||
<h1 className={styles.pageTitle}>{t('adminUserAccessOverview.benutzerzugriffsuebersicht')}</h1>
|
||||
<p className={styles.pageSubtitle}>{t('adminUserAccessOverview.zeigtAlleBerechtigungenEinesBenutzers')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -609,7 +613,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
disabled={loadingUsers}
|
||||
style={{ minWidth: '300px' }}
|
||||
>
|
||||
<option value="">-- Benutzer wählen --</option>
|
||||
<option value="">{t('adminUserAccessOverview.benutzerWaehlen')}</option>
|
||||
{users.map(user => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.fullName || user.username} ({user.email})
|
||||
|
|
@ -634,7 +638,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
{!selectedUserId ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaUserShield className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Benutzer auswählen</h3>
|
||||
<h3 className={styles.emptyTitle}>{t('adminUserAccessOverview.benutzerAuswaehlen')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Wählen Sie einen Benutzer aus, um dessen Zugriffsberechtigungen anzuzeigen.
|
||||
</p>
|
||||
|
|
@ -642,7 +646,7 @@ export const AdminUserAccessOverviewPage: React.FC = () => {
|
|||
) : loading ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Zugriffsübersicht...</span>
|
||||
<span>{t('adminUserAccessOverview.ladeZugriffsuebersicht')}</span>
|
||||
</div>
|
||||
) : overview ? (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -9,12 +9,16 @@ import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'
|
|||
import { useUserMandates, type MandateUser, type Mandate, type Role, type PaginationParams } from '../../hooks/useUserMandates';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaPlus, FaSync, FaUsers, FaBuilding } from 'react-icons/fa';
|
||||
import { FaPlus, FaSync, FaBuilding } from 'react-icons/fa';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import api from '../../api';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
export const AdminUserMandatesPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const { showError } = useToast();
|
||||
const {
|
||||
users,
|
||||
|
|
@ -97,7 +101,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
return [
|
||||
{
|
||||
key: 'username',
|
||||
label: 'Benutzername',
|
||||
label: t('adminUserMandates.benutzername'),
|
||||
type: 'text' as any,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -106,7 +110,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: 'E-Mail',
|
||||
label: t('adminUserMandates.email'),
|
||||
type: 'text' as any,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -115,7 +119,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'fullName',
|
||||
label: 'Vollständiger Name',
|
||||
label: t('adminUserMandates.vollstaendigerName'),
|
||||
type: 'text' as any,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -124,7 +128,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'roleLabels',
|
||||
label: 'Rollen',
|
||||
label: t('adminUserMandates.rollen'),
|
||||
type: 'text' as any,
|
||||
sortable: false,
|
||||
filterable: false,
|
||||
|
|
@ -137,7 +141,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
},
|
||||
{
|
||||
key: 'enabled',
|
||||
label: 'Aktiv',
|
||||
label: t('adminUserMandates.aktiv'),
|
||||
type: 'boolean' as any,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
|
|
@ -145,7 +149,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
width: 80,
|
||||
},
|
||||
];
|
||||
}, []); // No dependencies - columns are static, roleLabels come from backend
|
||||
}, [t]);
|
||||
|
||||
// Dynamic options for forms (users and roles)
|
||||
const userOptions = useMemo(() =>
|
||||
|
|
@ -274,7 +278,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Mandanten-Mitglieder</h1>
|
||||
<p className={styles.pageSubtitle}>Verwalten Sie, welche Benutzer Zugriff auf welche Mandanten haben</p>
|
||||
<p className={styles.pageSubtitle}>{t('adminUserMandates.verwaltenSieWelcheBenutzerZugriff')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -290,7 +294,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
value={selectedMandateId}
|
||||
onChange={(e) => setSelectedMandateId(e.target.value)}
|
||||
>
|
||||
<option value="">-- Mandant wählen --</option>
|
||||
<option value="">{t('adminUserMandates.mandantWaehlen')}</option>
|
||||
{mandates.map(m => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{getMandateName(m)}
|
||||
|
|
@ -323,7 +327,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
{!selectedMandateId ? (
|
||||
<div className={styles.emptyState}>
|
||||
<FaBuilding className={styles.emptyIcon} />
|
||||
<h3 className={styles.emptyTitle}>Kein Mandant ausgewählt</h3>
|
||||
<h3 className={styles.emptyTitle}>{t('adminUserMandates.keinMandantAusgewaehlt')}</h3>
|
||||
<p className={styles.emptyDescription}>
|
||||
Wählen Sie einen Mandanten aus, um dessen Mitglieder zu verwalten.
|
||||
</p>
|
||||
|
|
@ -345,11 +349,11 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEditClick,
|
||||
title: 'Rollen bearbeiten',
|
||||
title: t('adminUserMandatesPage.editRoles'),
|
||||
},
|
||||
{
|
||||
type: 'delete' as const,
|
||||
title: 'Aus Mandant entfernen',
|
||||
title: t('adminUserMandatesPage.removeFromMandate'),
|
||||
}
|
||||
]}
|
||||
onDelete={handleRemoveUser}
|
||||
|
|
@ -366,7 +370,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
return false;
|
||||
},
|
||||
}}
|
||||
emptyMessage="Keine Mitglieder gefunden"
|
||||
emptyMessage={t('adminUserMandates.keineMitgliederGefunden')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -376,7 +380,7 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
<div className={styles.modalOverlay} onClick={() => setShowAddModal(false)}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Benutzer zum Mandanten hinzufügen</h2>
|
||||
<h2 className={styles.modalTitle}>{t('adminUserMandates.benutzerZumMandantenHinzufuegen')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setShowAddModal(false)}
|
||||
|
|
@ -386,11 +390,11 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
{availableUsers.length === 0 ? (
|
||||
<p>Alle Benutzer sind bereits diesem Mandanten zugewiesen.</p>
|
||||
<p>{t('adminUserMandates.alleBenutzerSindBereitsDiesem')}</p>
|
||||
) : roleOptions.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Rollen...</span>
|
||||
<span>{t('adminUserMandates.ladeRollen')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
|
|
@ -398,8 +402,8 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
mode="create"
|
||||
onSubmit={handleAddUser}
|
||||
onCancel={() => setShowAddModal(false)}
|
||||
submitButtonText={isSubmitting ? 'Hinzufügen...' : 'Hinzufügen'}
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={isSubmitting ? t('adminUserMandates.hinzufuegen') : t('adminUserMandates.hinzufuegen')}
|
||||
cancelButtonText={t('adminUserMandates.abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -427,8 +431,8 @@ export const AdminUserMandatesPage: React.FC = () => {
|
|||
mode="edit"
|
||||
onSubmit={handleEditRoles}
|
||||
onCancel={() => setEditingUser(null)}
|
||||
submitButtonText={isSubmitting ? 'Speichern...' : 'Speichern'}
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={isSubmitting ? t('adminUserMandates.speichern') : t('adminUserMandates.speichern')}
|
||||
cancelButtonText={t('adminUserMandates.abbrechen')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ import { useNavigate } from 'react-router-dom';
|
|||
import { useOrgUsers, useUserOperations } from '../../hooks/useUsers';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaPlus, FaSync, FaUsers, FaKey, FaEnvelopeOpenText, FaUserShield } from 'react-icons/fa';
|
||||
import { FaPlus, FaSync, FaKey, FaEnvelopeOpenText, FaUserShield } from 'react-icons/fa';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
|
|
@ -23,6 +25,8 @@ interface User {
|
|||
}
|
||||
|
||||
export const AdminUsersPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const navigate = useNavigate();
|
||||
// Use two hooks: one for data, one for operations
|
||||
const {
|
||||
|
|
@ -141,8 +145,8 @@ export const AdminUsersPage: React.FC = () => {
|
|||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Benutzer</h1>
|
||||
<p className={styles.pageSubtitle}>Verwalten Sie alle Benutzer im System</p>
|
||||
<h1 className={styles.pageTitle}>{t('adminUsers.benutzer')}</h1>
|
||||
<p className={styles.pageSubtitle}>{t('adminUsers.verwaltenSieAlleBenutzerIm')}</p>
|
||||
</div>
|
||||
<div className={styles.headerActions}>
|
||||
<button
|
||||
|
|
@ -193,11 +197,11 @@ export const AdminUsersPage: React.FC = () => {
|
|||
...(canUpdate ? [{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEditClick,
|
||||
title: 'Bearbeiten',
|
||||
title: t('adminUsersPage.edit'),
|
||||
}] : []),
|
||||
...(canDelete ? [{
|
||||
type: 'delete' as const,
|
||||
title: 'Löschen',
|
||||
title: t('adminUsersPage.delete'),
|
||||
}] : []),
|
||||
]}
|
||||
customActions={canUpdate ? [
|
||||
|
|
@ -205,7 +209,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
id: 'sendPasswordLink',
|
||||
icon: <FaKey />,
|
||||
onClick: handleSendPassword,
|
||||
title: 'Passwort-Link senden',
|
||||
title: t('adminUsersPage.sendPasswordLink'),
|
||||
loading: (row: User) => sendingPasswordLinkState.has(row.id),
|
||||
}
|
||||
] : []}
|
||||
|
|
@ -218,7 +222,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
handleInlineUpdate,
|
||||
updateOptimistically,
|
||||
}}
|
||||
emptyMessage="Keine Benutzer gefunden"
|
||||
emptyMessage={t('adminUsers.keineBenutzerGefunden')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -227,7 +231,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
<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>
|
||||
<h2 className={styles.modalTitle}>{t('adminUsers.neuerBenutzer')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
|
|
@ -239,7 +243,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
{formAttributes.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Formular...</span>
|
||||
<span>{t('adminUsers.ladeFormular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
|
|
@ -247,8 +251,8 @@ export const AdminUsersPage: React.FC = () => {
|
|||
mode="create"
|
||||
onSubmit={handleCreateSubmit}
|
||||
onCancel={() => setShowCreateModal(false)}
|
||||
submitButtonText="Erstellen"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('adminUsers.erstellen')}
|
||||
cancelButtonText={t('adminUsers.abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -261,7 +265,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
<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>
|
||||
<h2 className={styles.modalTitle}>{t('adminUsers.benutzerBearbeiten')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setEditingUser(null)}
|
||||
|
|
@ -273,7 +277,7 @@ export const AdminUsersPage: React.FC = () => {
|
|||
{formAttributes.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Formular...</span>
|
||||
<span>{t('adminUsers.ladeFormular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
|
|
@ -282,8 +286,8 @@ export const AdminUsersPage: React.FC = () => {
|
|||
mode="edit"
|
||||
onSubmit={handleEditSubmit}
|
||||
onCancel={() => setEditingUser(null)}
|
||||
submitButtonText="Speichern"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('adminUsers.speichern')}
|
||||
cancelButtonText={t('adminUsers.abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import { TextField } from '../../components/UiComponents/TextField';
|
|||
import { useBilling } from '../../hooks/useBilling';
|
||||
import styles from './Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
const PROVIDER_LABELS: Record<string, string> = {
|
||||
anthropic: 'Anthropic (Claude)',
|
||||
openai: 'OpenAI (GPT)',
|
||||
|
|
@ -35,8 +37,7 @@ export interface ChatbotConfigSectionProps {
|
|||
onAllowedProvidersChange: (providers: string[]) => void;
|
||||
}
|
||||
|
||||
export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({
|
||||
connectors,
|
||||
export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({ connectors,
|
||||
systemPrompt,
|
||||
enableWebResearch,
|
||||
allowedProviders,
|
||||
|
|
@ -45,6 +46,7 @@ export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({
|
|||
onEnableWebResearchChange,
|
||||
onAllowedProvidersChange,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const { allowedProviders: availableProviders, loadAllowedProviders, loading: providersLoading } = useBilling();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -54,7 +56,7 @@ export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({
|
|||
}, []);
|
||||
|
||||
const availableConnectors = [
|
||||
{ id: 'preprocessor', label: 'Althaus Preprocessor', value: 'preprocessor' }
|
||||
{ id: 'preprocessor', label: t('chatbotConfigSection.althausPreprocessor'), value: 'preprocessor' }
|
||||
];
|
||||
|
||||
const handleConnectorToggle = (connectorValue: string) => {
|
||||
|
|
@ -110,7 +112,7 @@ export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({
|
|||
onChange={(e) => onEnableWebResearchChange(e.target.checked)}
|
||||
className={styles.multiselectCheckbox}
|
||||
/>
|
||||
<span>Web Research aktivieren (Tavily)</span>
|
||||
<span>{t('chatbotConfigSection.webResearchAktivierenTavily')}</span>
|
||||
</label>
|
||||
<p className={styles.configHelpText}>
|
||||
Wenn aktiviert, führt der Chatbot zusätzlich Web-Recherchen mit Tavily durch, um aktuelle Informationen aus dem Internet zu finden.
|
||||
|
|
@ -123,7 +125,7 @@ export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({
|
|||
</label>
|
||||
<div className={styles.multiselectContainer}>
|
||||
{providersLoading ? (
|
||||
<span style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>Lade Anbieter...</span>
|
||||
<span style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>{t('chatbotConfigSection.ladeAnbieter')}</span>
|
||||
) : (
|
||||
availableProviders.map(provider => (
|
||||
<label key={provider} className={styles.multiselectOption}>
|
||||
|
|
@ -155,7 +157,7 @@ export const ChatbotConfigSection: React.FC<ChatbotConfigSectionProps> = ({
|
|||
type="text"
|
||||
value={systemPrompt}
|
||||
onChange={onSystemPromptChange}
|
||||
placeholder="Benutzerdefinierter System-Prompt für den Chatbot..."
|
||||
placeholder={t('chatbotConfigSection.benutzerdefinierterSystempromptFuerDenChatbot')}
|
||||
className={styles.configTextArea}
|
||||
size="md"
|
||||
rows={6}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import { PermissionMatrix } from './PermissionMatrix';
|
|||
import styles from './Admin.module.css';
|
||||
import modalStyles from './InstanceDetailModal.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
export interface InstanceDetailModalProps {
|
||||
instance: FeatureInstance;
|
||||
mandateId: string;
|
||||
|
|
@ -24,8 +26,7 @@ export interface InstanceDetailModalProps {
|
|||
onSaved: () => void;
|
||||
}
|
||||
|
||||
export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({
|
||||
instance,
|
||||
export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({ instance,
|
||||
mandateId,
|
||||
mandateName,
|
||||
featureLabel,
|
||||
|
|
@ -42,6 +43,7 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({
|
|||
updateInstance,
|
||||
} = useFeatureAccess();
|
||||
const { showSuccess, showError } = useToast();
|
||||
const { t } = useLanguage();
|
||||
|
||||
const [users, setUsers] = useState<FeatureAccessUser[]>([]);
|
||||
const [roles, setRoles] = useState<Array<{ id: string; roleLabel: string }>>([]);
|
||||
|
|
@ -182,7 +184,7 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({
|
|||
() => [
|
||||
{
|
||||
name: 'userId',
|
||||
label: 'Benutzer',
|
||||
label: t('instanceDetailModal.benutzer'),
|
||||
type: 'enum' as const,
|
||||
required: true,
|
||||
options: availableUsers.map((u) => ({
|
||||
|
|
@ -192,7 +194,7 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({
|
|||
},
|
||||
{
|
||||
name: 'roleIds',
|
||||
label: 'Rollen',
|
||||
label: t('instanceDetailModal.rollen'),
|
||||
type: 'multiselect' as const,
|
||||
required: true,
|
||||
options: roleOptions as AttributeDefinition['options'],
|
||||
|
|
@ -205,14 +207,14 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({
|
|||
() => [
|
||||
{
|
||||
name: 'roleIds',
|
||||
label: 'Rollen',
|
||||
label: t('instanceDetailModal.rollen'),
|
||||
type: 'multiselect' as const,
|
||||
required: true,
|
||||
options: roleOptions as AttributeDefinition['options'],
|
||||
},
|
||||
{
|
||||
name: 'enabled',
|
||||
label: 'Aktiv',
|
||||
label: t('instanceDetailModal.aktiv'),
|
||||
type: 'checkbox' as const,
|
||||
required: false,
|
||||
},
|
||||
|
|
@ -223,13 +225,13 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({
|
|||
const tabs = [
|
||||
{
|
||||
id: 'users',
|
||||
label: 'Benutzer',
|
||||
label: t('instanceDetailModal.benutzer'),
|
||||
content: (
|
||||
<div className={modalStyles.tabContent}>
|
||||
{loading ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Benutzer...</span>
|
||||
<span>{t('instanceDetailModal.ladeBenutzer')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<PermissionMatrix
|
||||
|
|
@ -245,7 +247,7 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({
|
|||
},
|
||||
{
|
||||
id: 'roles',
|
||||
label: 'Rollen',
|
||||
label: t('instanceDetailModal.rollen'),
|
||||
content: (
|
||||
<div className={modalStyles.tabContent}>
|
||||
<p className={modalStyles.rolesIntro}>
|
||||
|
|
@ -269,19 +271,19 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({
|
|||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Einstellungen',
|
||||
label: t('instanceDetailModal.einstellungen'),
|
||||
content: (
|
||||
<div className={modalStyles.tabContent}>
|
||||
<FormGeneratorForm
|
||||
attributes={[
|
||||
{ name: 'label', type: 'string' as const, label: 'Bezeichnung', required: true, editable: true },
|
||||
{ name: 'enabled', type: 'boolean' as const, label: 'Aktiviert', required: false, editable: true },
|
||||
{ name: 'label', type: 'string' as const, label: t('instanceDetailModal.bezeichnung'), required: true, editable: true },
|
||||
{ name: 'enabled', type: 'boolean' as const, label: t('instanceDetailModal.aktiviert'), required: false, editable: true },
|
||||
]}
|
||||
data={instance}
|
||||
mode="edit"
|
||||
onSubmit={handleUpdateInstance}
|
||||
submitButtonText="Speichern"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('instanceDetailModal.speichern')}
|
||||
cancelButtonText={t('instanceDetailModal.abbrechen')}
|
||||
onCancel={() => {}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -299,7 +301,7 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({
|
|||
{mandateName} · {featureLabel}
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" className={styles.modalClose} onClick={onClose} aria-label="Schließen">
|
||||
<button type="button" className={styles.modalClose} onClick={onClose} aria-label={t('instanceDetailModal.schliessen')}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -312,24 +314,24 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({
|
|||
<div className={styles.modalOverlay} onClick={() => setShowAddModal(false)}>
|
||||
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Benutzer hinzufügen</h2>
|
||||
<h2 className={styles.modalTitle}>{t('instanceDetailModal.benutzerHinzufuegen')}</h2>
|
||||
<button type="button" className={styles.modalClose} onClick={() => setShowAddModal(false)}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.modalContent}>
|
||||
{availableUsers.length === 0 ? (
|
||||
<p>Alle Mandanten-Benutzer haben bereits Zugriff.</p>
|
||||
<p>{t('instanceDetailModal.alleMandantenbenutzerHabenBereitsZugriff')}</p>
|
||||
) : addUserFields.length < 2 || !roleOptions?.length ? (
|
||||
<p>Laden...</p>
|
||||
<p>{t('instanceDetailModal.laden')}</p>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
attributes={addUserFields}
|
||||
mode="create"
|
||||
onSubmit={handleAddUser}
|
||||
onCancel={() => setShowAddModal(false)}
|
||||
submitButtonText="Hinzufügen"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('instanceDetailModal.hinzufuegen')}
|
||||
cancelButtonText={t('instanceDetailModal.abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -353,8 +355,8 @@ export const InstanceDetailModal: React.FC<InstanceDetailModalProps> = ({
|
|||
mode="edit"
|
||||
onSubmit={handleUpdateRoles}
|
||||
onCancel={() => setEditingUser(null)}
|
||||
submitButtonText="Speichern"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('instanceDetailModal.speichern')}
|
||||
cancelButtonText={t('instanceDetailModal.abbrechen')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import type { Mandate } from '../../hooks/useUserMandates';
|
|||
import hubStyles from './AccessManagementHub.module.css';
|
||||
import hierarchyStyles from './InstanceHierarchyView.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
export interface InstanceHierarchyViewProps {
|
||||
mandates: Mandate[];
|
||||
getMandateName: (mandate: Mandate) => string;
|
||||
|
|
@ -53,6 +55,7 @@ function MandateContent({
|
|||
getFeatureLabel,
|
||||
onOpenDetail,
|
||||
}: MandateContentProps) {
|
||||
const { t } = useLanguage();
|
||||
const [expandedInstanceIds, setExpandedInstanceIds] = useState<Set<string>>(new Set());
|
||||
|
||||
const byFeature = useMemo(() => {
|
||||
|
|
@ -64,7 +67,6 @@ function MandateContent({
|
|||
return map;
|
||||
}, [instances]);
|
||||
|
||||
|
||||
const toggleInstance = (instanceId: string) => {
|
||||
setExpandedInstanceIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
|
|
@ -113,7 +115,7 @@ function MandateContent({
|
|||
<span
|
||||
className={`${hubStyles.instanceBadge} ${inst.enabled ? hubStyles.badgeActive : hubStyles.badgeInactive}`}
|
||||
>
|
||||
{inst.enabled ? 'Aktiv' : 'Inaktiv'}
|
||||
{inst.enabled ? t('instanceHierarchy.aktiv') : t('instanceHierarchy.inaktiv')}
|
||||
</span>
|
||||
<span className={hierarchyStyles.instanceUserCount}>
|
||||
<FaUsers /> {users.length}
|
||||
|
|
@ -126,7 +128,7 @@ function MandateContent({
|
|||
e.stopPropagation();
|
||||
onOpenDetail(inst, mandateId);
|
||||
}}
|
||||
title="Benutzer verwalten"
|
||||
title={t('instanceHierarchy.benutzerVerwalten')}
|
||||
>
|
||||
<FaUsers /> Benutzer verwalten
|
||||
</button>
|
||||
|
|
@ -170,6 +172,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
|
|||
loading,
|
||||
onOpenDetail,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [expandedMandateIds, setExpandedMandateIds] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleMandate = (mandateId: string) => {
|
||||
|
|
@ -186,7 +189,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
|
|||
<section className={hubStyles.section}>
|
||||
<div className={hierarchyStyles.hierarchyLoading}>
|
||||
<span className={hierarchyStyles.spinner} />
|
||||
<span>Lade Hierarchie und Benutzer...</span>
|
||||
<span>{t('instanceHierarchy.ladeHierarchieUndBenutzer')}</span>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
@ -195,7 +198,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
|
|||
if (mandates.length === 0) {
|
||||
return (
|
||||
<section className={hubStyles.section}>
|
||||
<h2 className={hubStyles.sectionTitle}>Hierarchie</h2>
|
||||
<h2 className={hubStyles.sectionTitle}>{t('instanceHierarchy.hierarchie')}</h2>
|
||||
<div className={hierarchyStyles.emptyHierarchy}>
|
||||
Keine Mandanten vorhanden. Legen Sie unter "Mandanten verwalten" einen Mandanten an.
|
||||
</div>
|
||||
|
|
@ -205,7 +208,7 @@ export const InstanceHierarchyView: React.FC<InstanceHierarchyViewProps> = ({
|
|||
|
||||
return (
|
||||
<section className={hubStyles.section}>
|
||||
<h2 className={hubStyles.sectionTitle}>Hierarchie</h2>
|
||||
<h2 className={hubStyles.sectionTitle}>{t('instanceHierarchy.hierarchie')}</h2>
|
||||
<div className={hierarchyStyles.hierarchyRoot}>
|
||||
{mandates.map((mandate) => {
|
||||
const mandateId = mandate.id;
|
||||
|
|
@ -259,12 +262,13 @@ interface UserRowProps {
|
|||
}
|
||||
|
||||
function UserRow({ user }: UserRowProps) {
|
||||
const { t } = useLanguage();
|
||||
const displayName = user.fullName?.trim() || user.username || user.userId;
|
||||
const rolesText =
|
||||
user.roleLabels && user.roleLabels.length > 0
|
||||
? user.roleLabels.join(', ')
|
||||
: 'Keine Rollen';
|
||||
const statusText = user.enabled ? 'Aktiv' : 'Inaktiv';
|
||||
const statusText = user.enabled ? t('instanceHierarchy.aktiv') : t('instanceHierarchy.inaktiv');
|
||||
|
||||
return (
|
||||
<div className={hierarchyStyles.userRowWrapper}>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import type { FeatureInstanceRole } from '../../hooks/useFeatureAccess';
|
|||
import styles from './Admin.module.css';
|
||||
import matrixStyles from './PermissionMatrix.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
export interface PermissionMatrixProps {
|
||||
users: FeatureAccessUser[];
|
||||
roles: FeatureInstanceRole[];
|
||||
|
|
@ -21,21 +23,21 @@ export interface PermissionMatrixProps {
|
|||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({
|
||||
users,
|
||||
export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({ users,
|
||||
roles,
|
||||
onEditUser,
|
||||
onRemoveUser,
|
||||
onAddUser,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
const [removingId, setRemovingId] = useState<string | null>(null);
|
||||
const { confirm, ConfirmDialog } = useConfirm();
|
||||
|
||||
const handleRemove = useCallback(async (user: FeatureAccessUser) => {
|
||||
if (removingId) return;
|
||||
const ok = await confirm(`"${user.username}" aus dieser Instanz entfernen?`, {
|
||||
title: 'Benutzer entfernen',
|
||||
title: t('permissionMatrix.removeUser'),
|
||||
confirmLabel: 'Entfernen',
|
||||
variant: 'danger',
|
||||
});
|
||||
|
|
@ -48,7 +50,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({
|
|||
if (roles.length === 0) {
|
||||
return (
|
||||
<div className={matrixStyles.empty}>
|
||||
<p>Keine Rollen in dieser Instanz. Bitte zuerst Rollen synchronisieren.</p>
|
||||
<p>{t('permissionMatrix.keineRollenInDieserInstanz')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -59,13 +61,13 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({
|
|||
<table className={matrixStyles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={matrixStyles.cellUser}>Benutzer</th>
|
||||
<th className={matrixStyles.cellUser}>{t('permissionMatrix.benutzer')}</th>
|
||||
{roles.map((r) => (
|
||||
<th key={r.id} className={matrixStyles.cellRole}>
|
||||
{r.roleLabel}
|
||||
</th>
|
||||
))}
|
||||
<th className={matrixStyles.cellActive}>Aktiv</th>
|
||||
<th className={matrixStyles.cellActive}>{t('permissionMatrix.aktiv')}</th>
|
||||
<th className={matrixStyles.cellActions}>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -111,7 +113,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({
|
|||
className={matrixStyles.actionBtn}
|
||||
onClick={() => onEditUser(user)}
|
||||
disabled={disabled}
|
||||
title="Rollen bearbeiten"
|
||||
title={t('permissionMatrix.rollenBearbeiten')}
|
||||
>
|
||||
<FaEdit />
|
||||
</button>
|
||||
|
|
@ -120,7 +122,7 @@ export const PermissionMatrix: React.FC<PermissionMatrixProps> = ({
|
|||
className={`${matrixStyles.actionBtn} ${matrixStyles.actionBtnDanger}`}
|
||||
onClick={() => handleRemove(user)}
|
||||
disabled={disabled || removingId === user.userId}
|
||||
title="Aus Instanz entfernen"
|
||||
title={t('permissionMatrix.ausInstanzEntfernen')}
|
||||
>
|
||||
<FaTrash />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import { useFeatureAccess, type FeatureInstance, type FeatureInstanceRole } from
|
|||
import { useToast } from '../../../contexts/ToastContext';
|
||||
import styles from '../Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
type InviteType = 'mandate' | 'featureInstance';
|
||||
|
||||
interface RoleOption {
|
||||
|
|
@ -74,6 +76,8 @@ const _cardStyle: React.CSSProperties = {
|
|||
// =============================================================================
|
||||
|
||||
export const AdminInvitationWizardPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const { showSuccess } = useToast();
|
||||
const { createInvitation } = useInvitations();
|
||||
const { fetchMandates, fetchRoles, fetchAllUsers, fetchMandateUsers } = useUserMandates();
|
||||
|
|
@ -169,17 +173,17 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
const email = inviteeForm.email.trim();
|
||||
const username = inviteeForm.username.trim();
|
||||
if (!email && !username) {
|
||||
setError('Bitte mindestens eine E-Mail-Adresse oder einen Benutzernamen angeben.');
|
||||
setError(t('adminInvitationWizard.bitteMindestensEineEmailadresseOder'));
|
||||
return;
|
||||
}
|
||||
const emailLower = email.toLowerCase();
|
||||
const userLower = username.toLowerCase();
|
||||
if (email && invitees.some(i => !i.isExisting && (i.email || '').toLowerCase() === emailLower)) {
|
||||
setError('Diese E-Mail ist bereits in der Liste');
|
||||
setError(t('adminInvitationWizard.dieseEmailIstBereitsIn'));
|
||||
return;
|
||||
}
|
||||
if (username && invitees.some(i => !i.isExisting && (i.username || '').toLowerCase() === userLower)) {
|
||||
setError('Dieser Benutzername ist bereits in der Liste');
|
||||
setError(t('adminInvitationWizard.dieserBenutzernameIstBereitsIn'));
|
||||
return;
|
||||
}
|
||||
setInvitees(prev => [...prev, {
|
||||
|
|
@ -194,14 +198,14 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
|
||||
const addInviteeExisting = () => {
|
||||
if (!selectedExistingUserId) {
|
||||
setError('Bitte wählen Sie einen Benutzer');
|
||||
setError(t('adminInvitationWizard.bitteWaehlenSieEinenBenutzer'));
|
||||
return;
|
||||
}
|
||||
const user = allSystemUsers.find(u => u.id === selectedExistingUserId);
|
||||
if (!user) return;
|
||||
const email = (user.email || '').trim();
|
||||
if (invitees.some(i => i.userId === user.id)) {
|
||||
setError('Dieser Benutzer ist bereits in der Liste');
|
||||
setError(t('adminInvitationWizard.dieserBenutzerIstBereitsIn'));
|
||||
return;
|
||||
}
|
||||
setInvitees(prev => [...prev, {
|
||||
|
|
@ -231,7 +235,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
const handleSend = async () => {
|
||||
if (!selectedMandate || invitees.length === 0) return;
|
||||
if (inviteType === 'featureInstance' && !selectedInstance) {
|
||||
setError('Bitte wählen Sie eine Feature-Instanz aus.');
|
||||
setError(t('adminInvitationWizard.bitteWaehlenSieEineFeatureinstanz'));
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
|
|
@ -353,7 +357,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
{/* ── STEP 1: Invite type ── */}
|
||||
{step === 1 && (
|
||||
<div style={_cardStyle}>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>Wohin möchten Sie einladen?</h3>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>{t('adminInvitationWizard.wohinMoechtenSieEinladen')}</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
|
||||
<button
|
||||
onClick={() => setInviteType('mandate')}
|
||||
|
|
@ -363,7 +367,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
cursor: 'pointer', textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '4px' }}>Zum Mandanten</div>
|
||||
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '4px' }}>{t('adminInvitationWizard.zumMandanten')}</div>
|
||||
<div style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
|
||||
Einladung zum Mandanten ohne spezifische Feature-Instanz
|
||||
</div>
|
||||
|
|
@ -376,7 +380,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
cursor: 'pointer', textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '4px' }}>Zur Feature-Instanz</div>
|
||||
<div style={{ fontWeight: 600, fontSize: '14px', marginBottom: '4px' }}>{t('adminInvitationWizard.zurFeatureinstanz')}</div>
|
||||
<div style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>
|
||||
Einladung zu einer bestimmten Feature-Instanz mit Rolle
|
||||
</div>
|
||||
|
|
@ -394,10 +398,10 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
{step === 2 && (
|
||||
<div style={_cardStyle}>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>
|
||||
{inviteType === 'mandate' ? 'Mandant auswählen' : 'Mandant und Feature-Instanz auswählen'}
|
||||
{inviteType === 'mandate' ? t('adminInvitationWizard.mandantAuswaehlen') : t('adminInvitationWizard.mandantUndFeatureinstanzAuswaehlen')}
|
||||
</h3>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label className={styles.formLabel}>Mandant *</label>
|
||||
<label className={styles.formLabel}>{t('adminInvitationWizard.mandant')}</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
style={{ width: '100%' }}
|
||||
|
|
@ -408,7 +412,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
setSelectedInstance(null);
|
||||
}}
|
||||
>
|
||||
<option value="">-- Mandant wählen --</option>
|
||||
<option value="">{t('adminInvitationWizard.mandantWaehlen')}</option>
|
||||
{mandates.map(m => (
|
||||
<option key={m.id} value={m.id}>{getMandateName(m)}</option>
|
||||
))}
|
||||
|
|
@ -416,9 +420,9 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
</div>
|
||||
{inviteType === 'featureInstance' && selectedMandate && (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label className={styles.formLabel}>Feature-Instanz *</label>
|
||||
<label className={styles.formLabel}>{t('adminInvitationWizard.featureinstanz')}</label>
|
||||
{instances.length === 0 ? (
|
||||
<p style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>Keine Feature-Instanzen für diesen Mandanten vorhanden.</p>
|
||||
<p style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>{t('adminInvitationWizard.keineFeatureinstanzenFuerDiesenMandanten')}</p>
|
||||
) : (
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
|
|
@ -429,7 +433,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
setSelectedInstance(inst || null);
|
||||
}}
|
||||
>
|
||||
<option value="">-- Feature-Instanz wählen --</option>
|
||||
<option value="">{t('adminInvitationWizard.featureinstanzWaehlen')}</option>
|
||||
{instances.map(inst => {
|
||||
const baseLabel = inst.label || inst.featureCode;
|
||||
const suffix = inst.enabled === false ? ' (deaktiviert)' : '';
|
||||
|
|
@ -442,7 +446,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button className={styles.secondaryButton} onClick={() => setStep(1)}>← Zurück</button>
|
||||
<button className={styles.secondaryButton} onClick={() => setStep(1)}>{t('adminInvitationWizard.larrZurueck')}</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
disabled={!canProceedStep3}
|
||||
|
|
@ -458,7 +462,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
{step === 3 && selectedMandate && (
|
||||
<div>
|
||||
<div style={_cardStyle}>
|
||||
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>Einladungen hinzufügen</h3>
|
||||
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px' }}>{t('adminInvitationWizard.einladungenHinzufuegen')}</h3>
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: '13px', margin: '0 0 16px 0' }}>
|
||||
Für neue Benutzer: mindestens eine E-Mail <em>oder</em> ein Benutzername (vorgegeben). Ohne E-Mail wird kein Link per Mail versendet — der Einladungslink kann manuell geteilt werden. Bestehende Benutzer wählen Sie im zweiten Tab.
|
||||
</p>
|
||||
|
|
@ -484,7 +488,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
{addMode === 'email' ? (
|
||||
<div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}>
|
||||
<div>
|
||||
<label className={styles.formLabel}>E-Mail (optional)</label>
|
||||
<label className={styles.formLabel}>{t('adminInvitationWizard.emailOptional')}</label>
|
||||
<input
|
||||
className={styles.formInput}
|
||||
type="email"
|
||||
|
|
@ -494,14 +498,14 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={styles.formLabel}>Benutzername (optional)</label>
|
||||
<label className={styles.formLabel}>{t('adminInvitationWizard.benutzernameOptional')}</label>
|
||||
<input
|
||||
className={styles.formInput}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
value={inviteeForm.username}
|
||||
onChange={e => setInviteeForm(p => ({ ...p, username: e.target.value }))}
|
||||
placeholder="z. B. vorname.nachname"
|
||||
placeholder={t('adminInvitationWizard.zBVornamenachname')}
|
||||
/>
|
||||
<p style={{ fontSize: '11px', color: 'var(--text-secondary)', marginTop: '4px' }}>
|
||||
Mindestens eines der beiden Felder ausfüllen. Mit Benutzername muss der Eingeladene genau diesen Namen beim Annehmen verwenden.
|
||||
|
|
@ -549,25 +553,25 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
) : (
|
||||
<div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}>
|
||||
<div>
|
||||
<label className={styles.formLabel}>Bestehender Benutzer *</label>
|
||||
<label className={styles.formLabel}>{t('adminInvitationWizard.bestehenderBenutzer')}</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
style={{ width: '100%' }}
|
||||
value={selectedExistingUserId}
|
||||
onChange={e => setSelectedExistingUserId(e.target.value)}
|
||||
>
|
||||
<option value="">-- Benutzer wählen --</option>
|
||||
<option value="">{t('adminInvitationWizard.benutzerWaehlen')}</option>
|
||||
{availableExistingUsers.map(u => (
|
||||
<option key={u.id} value={u.id}>
|
||||
{u.username} {u.email ? `(${u.email})` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{availableExistingUsers.length === 0 && <p style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '4px' }}>Keine weiteren Benutzer verfügbar.</p>}
|
||||
{availableExistingUsers.length === 0 && <p style={{ fontSize: '12px', color: 'var(--text-secondary)', marginTop: '4px' }}>{t('adminInvitationWizard.keineWeiterenBenutzerVerfuegbar')}</p>}
|
||||
</div>
|
||||
{roles.length > 0 && (
|
||||
<div>
|
||||
<label className={styles.formLabel}>Rolle *</label>
|
||||
<label className={styles.formLabel}>{t('adminInvitationWizard.rolle')}</label>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
{roles.map(r => (
|
||||
<label key={r.id} className={styles.checkboxLabel} style={{
|
||||
|
|
@ -608,9 +612,9 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid var(--border-color, #C5D9E8)' }}>
|
||||
<th style={{ textAlign: 'left', padding: '8px' }}>E-Mail / Benutzer</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px' }}>{t('adminInvitationWizard.emailBenutzer')}</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px' }}>Benutzername</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px' }}>Rollen</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px' }}>{t('adminInvitationWizard.rollen')}</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px' }}>Typ</th>
|
||||
<th style={{ textAlign: 'right', padding: '8px' }}>Aktion</th>
|
||||
</tr>
|
||||
|
|
@ -641,12 +645,12 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
</tbody>
|
||||
</table>
|
||||
) : (
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: '13px', marginBottom: '16px' }}>Noch keine Einladungen hinzugefügt.</p>
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: '13px', marginBottom: '16px' }}>{t('adminInvitationWizard.nochKeineEinladungenHinzugefuegt')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '16px', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button className={styles.secondaryButton} onClick={() => setStep(2)}>← Zurück</button>
|
||||
<button className={styles.secondaryButton} onClick={() => setStep(2)}>{t('adminInvitationWizard.larrZurueck')}</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
disabled={invitees.length === 0}
|
||||
|
|
@ -661,12 +665,12 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
{/* ── STEP 4: Summary and send ── */}
|
||||
{step === 4 && selectedMandate && !dispatchResults && (
|
||||
<div style={_cardStyle}>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>Zusammenfassung & Versand</h3>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>{t('adminInvitationWizard.zusammenfassungVersand')}</h3>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<strong>Art:</strong> {inviteType === 'mandate' ? 'Einladung zum Mandanten' : 'Einladung zur Feature-Instanz'}
|
||||
<strong>Art:</strong> {inviteType === 'mandate' ? t('adminInvitationWizard.einladungZumMandanten') : t('adminInvitationWizard.einladungZurFeatureinstanz')}
|
||||
</div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<strong>Mandant:</strong> {getMandateName(selectedMandate)}
|
||||
<strong>{t('adminInvitationWizard.mandant')}</strong> {getMandateName(selectedMandate)}
|
||||
</div>
|
||||
{inviteType === 'featureInstance' && selectedInstance && (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
|
|
@ -686,7 +690,7 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
</ul>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '24px' }}>
|
||||
<button className={styles.secondaryButton} onClick={() => setStep(3)}>← Zurück</button>
|
||||
<button className={styles.secondaryButton} onClick={() => setStep(3)}>{t('adminInvitationWizard.larrZurueck')}</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
disabled={isLoading}
|
||||
|
|
@ -701,13 +705,13 @@ export const AdminInvitationWizardPage: React.FC = () => {
|
|||
{/* ── Results ── */}
|
||||
{dispatchResults && (
|
||||
<div style={_cardStyle}>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>Ergebnis</h3>
|
||||
<h3 style={{ margin: '0 0 16px 0', fontSize: '16px' }}>{t('adminInvitationWizard.ergebnis')}</h3>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '13px' }}>
|
||||
<thead>
|
||||
<tr style={{ borderBottom: '2px solid var(--border-color, #C5D9E8)' }}>
|
||||
<th style={{ textAlign: 'left', padding: '8px' }}>E-Mail / Benutzer</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px' }}>Status</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px' }}>E-Mail gesendet</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px' }}>{t('adminInvitationWizard.emailBenutzer')}</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px' }}>{t('adminInvitationWizard.status')}</th>
|
||||
<th style={{ textAlign: 'left', padding: '8px' }}>{t('adminInvitationWizard.emailGesendet')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import { splitMandateAndBillingFromForm } from '../../../utils/mandateBillingFor
|
|||
import { FormGeneratorForm } from '../../../components/FormGenerator/FormGeneratorForm';
|
||||
import styles from '../Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
const TOTAL_STEPS = 4;
|
||||
const STEP_LABELS = ['Mandant', 'Benutzer', 'Instances', 'Feature-Benutzer'];
|
||||
|
||||
|
|
@ -30,6 +32,8 @@ interface RoleOption {
|
|||
}
|
||||
|
||||
export const AdminMandateWizardPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
const { showSuccess, showWarning, showError } = useToast();
|
||||
const { request } = useApiRequest();
|
||||
const {
|
||||
|
|
@ -133,7 +137,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
const data = await fetchMandatesList();
|
||||
setMandates(data);
|
||||
} catch {
|
||||
setError('Fehler beim Laden der Mandanten');
|
||||
setError(t('adminMandateWizard.fehlerBeimLadenDerMandanten'));
|
||||
}
|
||||
}, [fetchMandatesList]);
|
||||
|
||||
|
|
@ -456,10 +460,10 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ background: 'var(--bg-secondary, #f8fafc)' }}>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>Benutzer</th>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>{t('adminMandateWizard.benutzer')}</th>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>E-Mail</th>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>Rollen</th>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'center', fontSize: '12px', fontWeight: 600 }}>Status</th>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'left', fontSize: '12px', fontWeight: 600 }}>{t('adminMandateWizard.rollen')}</th>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'center', fontSize: '12px', fontWeight: 600 }}>{t('adminMandateWizard.status')}</th>
|
||||
<th style={{ padding: '8px 12px', textAlign: 'right', fontSize: '12px', fontWeight: 600 }}>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -496,7 +500,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
background: u.enabled !== false ? '#dcfce7' : 'var(--bg-secondary)',
|
||||
color: u.enabled !== false ? '#166534' : 'var(--text-secondary)',
|
||||
}}>
|
||||
{u.enabled !== false ? 'Aktiv' : 'Inaktiv'}
|
||||
{u.enabled !== false ? t('adminMandateWizard.aktiv') : t('adminMandateWizard.inaktiv')}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '8px 12px', textAlign: 'right' }}>
|
||||
|
|
@ -532,7 +536,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
) => (
|
||||
<div style={{ padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px', marginBottom: '16px', display: 'grid', gap: '12px' }}>
|
||||
<div>
|
||||
<label className={styles.formLabel}>Benutzer * (mehrfach möglich)</label>
|
||||
<label className={styles.formLabel}>{t('adminMandateWizard.benutzerMehrfachMoeglich')}</label>
|
||||
<div
|
||||
style={{
|
||||
maxHeight: '220px',
|
||||
|
|
@ -573,7 +577,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
</div>
|
||||
{roles.length > 0 && (
|
||||
<div>
|
||||
<label className={styles.formLabel}>Rollen (für alle ausgewählten Benutzer)</label>
|
||||
<label className={styles.formLabel}>{t('adminMandateWizard.rollenFuerAlleAusgewaehltenBenutzer')}</label>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
{roles.map(r => (
|
||||
<label key={r.id} className={styles.checkboxLabel}>
|
||||
|
|
@ -601,7 +605,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
onClick={onSubmit}
|
||||
disabled={isLoading || formValue.userIds.length === 0}
|
||||
>
|
||||
{isLoading ? 'Hinzufügen...' : 'Hinzufügen'}
|
||||
{isLoading ? t('adminMandateWizard.hinzufuegen') : t('adminMandateWizard.hinzufuegen')}
|
||||
</button>
|
||||
<button className={styles.secondaryButton} onClick={onCancel}>
|
||||
Abbrechen
|
||||
|
|
@ -668,7 +672,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Mandanten-Verwaltung</h1>
|
||||
<p className={styles.pageSubtitle}>Schritt-für-Schritt Wizard zur Mandanten-Konfiguration</p>
|
||||
<p className={styles.pageSubtitle}>{t('adminMandateWizard.schrittfuerschrittWizardZurMandantenkonfiguration')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -688,7 +692,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
{/* ── STEP 1: MANDATE ── */}
|
||||
{step === 1 && (
|
||||
<div style={cardStyle}>
|
||||
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px', marginTop: 0 }}>Mandant auswählen oder erstellen</h3>
|
||||
<h3 style={{ fontSize: '15px', fontWeight: 600, marginBottom: '16px', marginTop: 0 }}>{t('adminMandateWizard.mandantAuswaehlenOderErstellen')}</h3>
|
||||
|
||||
{!isCreatingMandate ? (
|
||||
<>
|
||||
|
|
@ -723,7 +727,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
{mandateAttrLoading || createFormAttributes.length === 0 ? (
|
||||
<div className={styles.loadingContainer} style={{ padding: '24px' }}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Formular wird geladen...</span>
|
||||
<span>{t('adminMandateWizard.formularWirdGeladen')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
|
|
@ -731,8 +735,8 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
mode="create"
|
||||
onSubmit={handleCreateMandate}
|
||||
onCancel={() => setIsCreatingMandate(false)}
|
||||
submitButtonText={isLoading ? 'Erstellen...' : 'Mandant erstellen'}
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={isLoading ? t('adminMandateWizard.erstellen') : t('adminMandateWizard.mandantErstellen')}
|
||||
cancelButtonText={t('adminMandateWizard.abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -785,7 +789,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
gap: '12px',
|
||||
border: '1px solid var(--border-color, #e5e7eb)',
|
||||
}}>
|
||||
<div style={{ fontWeight: 600, fontSize: '14px' }}>Rollen bearbeiten</div>
|
||||
<div style={{ fontWeight: 600, fontSize: '14px' }}>{t('adminMandateWizard.rollenBearbeiten')}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
{mandateRoles.map(r => (
|
||||
<label key={r.id} className={styles.checkboxLabel}>
|
||||
|
|
@ -833,7 +837,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button className={styles.secondaryButton} onClick={() => setStep(1)}>← Zurück</button>
|
||||
<button className={styles.secondaryButton} onClick={() => setStep(1)}>{t('adminMandateWizard.larrZurueck')}</button>
|
||||
<button className={styles.primaryButton} onClick={() => setStep(3)}>
|
||||
Weiter →
|
||||
</button>
|
||||
|
|
@ -855,13 +859,13 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
|
||||
{/* Feature Filter */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label className={styles.formLabel}>Feature filtern:</label>
|
||||
<label className={styles.formLabel}>{t('adminMandateWizard.featureFiltern')}</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
value={selectedFeatureCode}
|
||||
onChange={e => setSelectedFeatureCode(e.target.value)}
|
||||
>
|
||||
<option value="">Alle Features</option>
|
||||
<option value="">{t('adminMandateWizard.alleFeatures')}</option>
|
||||
{features.map(f => (
|
||||
<option key={f.code} value={f.code}>{getFeatureLabel(f.code)}</option>
|
||||
))}
|
||||
|
|
@ -879,7 +883,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
<div>
|
||||
<span style={{ fontWeight: 600 }}>{inst.label}</span>
|
||||
<span style={{ fontSize: '11px', color: 'var(--text-secondary)', marginLeft: '8px' }}>
|
||||
{getFeatureLabel(inst.featureCode)} | {inst.enabled ? 'Aktiv' : 'Inaktiv'}
|
||||
{getFeatureLabel(inst.featureCode)} | {inst.enabled ? t('adminMandateWizard.aktiv') : t('adminMandateWizard.inaktiv')}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
|
|
@ -911,26 +915,26 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
) : (
|
||||
<div style={{ display: 'grid', gap: '12px', padding: '16px', background: 'var(--bg-secondary, #f8fafc)', borderRadius: '8px' }}>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.formLabel}>Feature *</label>
|
||||
<label className={styles.formLabel}>{t('adminMandateWizard.feature')}</label>
|
||||
<select
|
||||
className={styles.filterSelect}
|
||||
style={{ width: '100%' }}
|
||||
value={selectedFeatureCode}
|
||||
onChange={e => setSelectedFeatureCode(e.target.value)}
|
||||
>
|
||||
<option value="">-- Feature wählen --</option>
|
||||
<option value="">{t('adminMandateWizard.featureWaehlen')}</option>
|
||||
{features.map(f => (
|
||||
<option key={f.code} value={f.code}>{getFeatureLabel(f.code)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={`${styles.formLabel} ${styles.required}`}>Bezeichnung</label>
|
||||
<label className={`${styles.formLabel} ${styles.required}`}>{t('adminMandateWizard.bezeichnung')}</label>
|
||||
<input
|
||||
className={styles.formInput}
|
||||
value={instanceForm.label}
|
||||
onChange={e => setInstanceForm(p => ({ ...p, label: e.target.value }))}
|
||||
placeholder="z.B. Kunde A"
|
||||
placeholder={t('adminMandateWizard.zbKundeA')}
|
||||
/>
|
||||
</div>
|
||||
<label className={styles.checkboxLabel}>
|
||||
|
|
@ -943,15 +947,15 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
</label>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<button className={styles.primaryButton} onClick={handleCreateInstance} disabled={isLoading || !selectedFeatureCode}>
|
||||
{isLoading ? 'Erstellen...' : 'Erstellen'}
|
||||
{isLoading ? t('adminMandateWizard.erstellen') : t('adminMandateWizard.erstellen')}
|
||||
</button>
|
||||
<button className={styles.secondaryButton} onClick={() => setIsCreatingInstance(false)}>Abbrechen</button>
|
||||
<button className={styles.secondaryButton} onClick={() => setIsCreatingInstance(false)}>{t('adminMandateWizard.abbrechen')}</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button className={styles.secondaryButton} onClick={() => setStep(2)}>← Zurück</button>
|
||||
<button className={styles.secondaryButton} onClick={() => setStep(2)}>{t('adminMandateWizard.larrZurueck')}</button>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={() => { if (instances.length > 0) { setSelectedInstance(instances[0]); setStep(4); } }}
|
||||
|
|
@ -1002,7 +1006,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
gap: '12px',
|
||||
border: '1px solid var(--border-color, #e5e7eb)',
|
||||
}}>
|
||||
<div style={{ fontWeight: 600, fontSize: '14px' }}>Rollen bearbeiten (Feature-Instanz)</div>
|
||||
<div style={{ fontWeight: 600, fontSize: '14px' }}>{t('adminMandateWizard.rollenBearbeitenFeatureinstanz')}</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
{instanceRoles.map(r => (
|
||||
<label key={r.id} className={styles.checkboxLabel}>
|
||||
|
|
@ -1055,7 +1059,7 @@ export const AdminMandateWizardPage: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<div style={{ marginTop: '24px', display: 'flex', justifyContent: 'space-between' }}>
|
||||
<button className={styles.secondaryButton} onClick={() => setStep(3)}>← Zurück</button>
|
||||
<button className={styles.secondaryButton} onClick={() => setStep(3)}>{t('adminMandateWizard.larrZurueck')}</button>
|
||||
<button className={styles.primaryButton} onClick={() => {
|
||||
showSuccess('Fertig', 'Konfiguration abgeschlossen!');
|
||||
setSelectedInstance(null);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import type { Feature } from '../../../hooks/useFeatureAccess';
|
|||
import styles from '../Admin.module.css';
|
||||
import wizardStyles from './FeatureInstanceWizard.module.css';
|
||||
|
||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
|
||||
function getMandateName(m: Mandate): string {
|
||||
if (typeof m.name === 'object') return m.name.de || m.name.en || Object.values(m.name)[0] || m.id;
|
||||
return m.name || m.id;
|
||||
|
|
@ -38,8 +40,7 @@ const STEPS = [
|
|||
{ id: 'users', title: 'Benutzer (optional)' },
|
||||
];
|
||||
|
||||
export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({
|
||||
mandateId: initialMandateId,
|
||||
export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({ mandateId: initialMandateId,
|
||||
mandates,
|
||||
features,
|
||||
onClose,
|
||||
|
|
@ -47,6 +48,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({
|
|||
}) => {
|
||||
const { createInstance, addUserToInstance, fetchInstanceRoles } = useFeatureAccess();
|
||||
const { showSuccess, showError } = useToast();
|
||||
const { t } = useLanguage();
|
||||
|
||||
const [step, setStep] = useState(0);
|
||||
const [mandateId, setMandateId] = useState(initialMandateId || '');
|
||||
|
|
@ -71,10 +73,10 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({
|
|||
|
||||
const createFields: AttributeDefinition[] = useMemo(
|
||||
() => [
|
||||
{ name: 'mandateId', label: 'Mandant', type: 'enum' as const, required: true, options: mandateOptions },
|
||||
{ name: 'featureCode', label: 'Feature', type: 'enum' as const, required: true, options: featureOptions },
|
||||
{ name: 'label', label: 'Bezeichnung', type: 'string' as const, required: true, editable: true },
|
||||
{ name: 'enabled', label: 'Aktiv', type: 'boolean' as const, required: false, editable: true },
|
||||
{ name: 'mandateId', label: t('featureInstanceWizard.mandant'), type: 'enum' as const, required: true, options: mandateOptions },
|
||||
{ name: 'featureCode', label: t('featureInstanceWizard.feature'), type: 'enum' as const, required: true, options: featureOptions },
|
||||
{ name: 'label', label: t('featureInstanceWizard.bezeichnung'), type: 'string' as const, required: true, editable: true },
|
||||
{ name: 'enabled', label: t('featureInstanceWizard.aktiv'), type: 'boolean' as const, required: false, editable: true },
|
||||
],
|
||||
[mandateOptions, featureOptions]
|
||||
);
|
||||
|
|
@ -172,8 +174,8 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({
|
|||
<div className={styles.modalOverlay} onClick={onClose}>
|
||||
<div className={`${styles.modal} ${wizardStyles.modal}`} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Neue Feature-Instanz</h2>
|
||||
<button type="button" className={styles.modalClose} onClick={onClose} aria-label="Schließen">
|
||||
<h2 className={styles.modalTitle}>{t('featureInstanceWizard.neueFeatureinstanz')}</h2>
|
||||
<button type="button" className={styles.modalClose} onClick={onClose} aria-label={t('featureInstanceWizard.schliessen')}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -204,8 +206,8 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({
|
|||
}}
|
||||
onSubmit={handleStep1Submit}
|
||||
onCancel={onClose}
|
||||
submitButtonText="Weiter"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('featureInstanceWizard.weiter')}
|
||||
cancelButtonText={t('featureInstanceWizard.abbrechen')}
|
||||
/>
|
||||
<label className={wizardStyles.checkLabel}>
|
||||
<input
|
||||
|
|
@ -240,7 +242,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({
|
|||
Optional: Weisen Sie Benutzern Rollen zu. Sie können dies auch später in der Zugriffsverwaltung tun.
|
||||
</p>
|
||||
{mandateUsers.length === 0 ? (
|
||||
<p className={wizardStyles.stepText}>Keine Mandanten-Benutzer vorhanden.</p>
|
||||
<p className={wizardStyles.stepText}>{t('featureInstanceWizard.keineMandantenbenutzerVorhanden')}</p>
|
||||
) : (
|
||||
<div className={wizardStyles.userList}>
|
||||
{mandateUsers.map((u) => {
|
||||
|
|
@ -258,7 +260,7 @@ export const FeatureInstanceWizard: React.FC<FeatureInstanceWizardProps> = ({
|
|||
handleAddUserRole(u.id, rids);
|
||||
}}
|
||||
>
|
||||
<option value="">— Keine Rolle —</option>
|
||||
<option value="">{t('featureInstanceWizard.keineRolle')}</option>
|
||||
{instanceRoles.map((r) => (
|
||||
<option key={r.id} value={r.id}>
|
||||
{r.roleLabel}
|
||||
|
|
|
|||
|
|
@ -9,14 +9,18 @@ import React, { useState, useMemo, useEffect } from 'react';
|
|||
import { useConnections, type Connection } from '../../hooks/useConnections';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
import { FaSync, FaPlug, FaGoogle, FaMicrosoft, FaLink, FaRedo, FaShieldAlt, FaTasks } from 'react-icons/fa';
|
||||
import { FaSync, FaGoogle, FaMicrosoft, FaLink, FaRedo, FaShieldAlt, FaTasks } from 'react-icons/fa';
|
||||
import { getApiBaseUrl } from '../../../config/config';
|
||||
import styles from '../admin/Admin.module.css';
|
||||
|
||||
import { useLanguage } from '../../providers/language/LanguageContext';
|
||||
|
||||
/** Wenn false: keine neue ClickUp-Verbindung über diese Seite (Buttons inaktiv). */
|
||||
const isClickupConnectionUiEnabled = false;
|
||||
|
||||
export const ConnectionsPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
// Use the consolidated hook
|
||||
const {
|
||||
data: connections,
|
||||
|
|
@ -247,7 +251,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
|
||||
<div className={styles.pageHeader}>
|
||||
<div>
|
||||
<h1 className={styles.pageTitle}>Verbindungen</h1>
|
||||
<h1 className={styles.pageTitle}>{t('connections.verbindungen')}</h1>
|
||||
<p className={styles.pageSubtitle}>
|
||||
Persönliche Datenanbindungen verwalten (OAuth: Google, Microsoft
|
||||
{isClickupConnectionUiEnabled ? ', ClickUp' : ''})
|
||||
|
|
@ -258,7 +262,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
className={styles.secondaryButton}
|
||||
onClick={handleAdminConsent}
|
||||
disabled={adminConsentPending}
|
||||
title="Microsoft Admin Consent — erteilt der App die nötigen Berechtigungen für den gesamten Tenant"
|
||||
title={t('connections.microsoftAdminConsentErteiltDer')}
|
||||
>
|
||||
<FaShieldAlt /> Admin Consent
|
||||
</button>
|
||||
|
|
@ -291,7 +295,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
className={styles.clickupButton}
|
||||
onClick={handleCreateClickup}
|
||||
disabled={isConnecting}
|
||||
title="ClickUp-Konto verbinden"
|
||||
title={t('connections.clickupkontoVerbinden')}
|
||||
>
|
||||
<FaTasks /> ClickUp
|
||||
</button>
|
||||
|
|
@ -317,11 +321,11 @@ export const ConnectionsPage: React.FC = () => {
|
|||
...(canUpdate ? [{
|
||||
type: 'edit' as const,
|
||||
onAction: handleEditClick,
|
||||
title: 'Bearbeiten',
|
||||
title: t('connectionsPage.edit'),
|
||||
}] : []),
|
||||
...(canDelete ? [{
|
||||
type: 'delete' as const,
|
||||
title: 'Löschen',
|
||||
title: t('connectionsPage.delete'),
|
||||
loading: (row: Connection) => deletingConnections.has(row.id),
|
||||
}] : []),
|
||||
]}
|
||||
|
|
@ -330,7 +334,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
id: 'connect',
|
||||
icon: <FaLink />,
|
||||
onClick: handleConnect,
|
||||
title: 'Verbinden',
|
||||
title: t('connectionsPage.connect'),
|
||||
visible: (row: Connection) =>
|
||||
row.status !== 'active' &&
|
||||
(isClickupConnectionUiEnabled || row.authority !== 'clickup'),
|
||||
|
|
@ -340,7 +344,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
id: 'refresh',
|
||||
icon: <FaRedo />,
|
||||
onClick: handleRefresh,
|
||||
title: 'Token erneuern',
|
||||
title: t('connectionsPage.refreshToken'),
|
||||
visible: (row: Connection) => row.status === 'active',
|
||||
loading: (row: Connection) => refreshingConnections.has(row.id),
|
||||
},
|
||||
|
|
@ -354,7 +358,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
handleInlineUpdate,
|
||||
updateOptimistically,
|
||||
}}
|
||||
emptyMessage="Keine Verbindungen gefunden"
|
||||
emptyMessage={t('connections.keineVerbindungenGefunden')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -363,7 +367,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
<div className={styles.modalOverlay} onClick={() => setEditingConnection(null)}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
<div className={styles.modalHeader}>
|
||||
<h2 className={styles.modalTitle}>Verbindung bearbeiten</h2>
|
||||
<h2 className={styles.modalTitle}>{t('connections.verbindungBearbeiten')}</h2>
|
||||
<button
|
||||
className={styles.modalClose}
|
||||
onClick={() => setEditingConnection(null)}
|
||||
|
|
@ -375,7 +379,7 @@ export const ConnectionsPage: React.FC = () => {
|
|||
{formAttributes.length === 0 ? (
|
||||
<div className={styles.loadingContainer}>
|
||||
<div className={styles.spinner} />
|
||||
<span>Lade Formular...</span>
|
||||
<span>{t('connections.ladeFormular')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<FormGeneratorForm
|
||||
|
|
@ -384,8 +388,8 @@ export const ConnectionsPage: React.FC = () => {
|
|||
mode="edit"
|
||||
onSubmit={handleEditSubmit}
|
||||
onCancel={() => setEditingConnection(null)}
|
||||
submitButtonText="Speichern"
|
||||
cancelButtonText="Abbrechen"
|
||||
submitButtonText={t('connections.speichern')}
|
||||
cancelButtonText={t('connections.abbrechen')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue